using AssortGenerator.Common; using AssortGenerator.Common.Helpers; using AssortGenerator.Models.Input; using AssortGenerator.Models.Other; using AssortGenerator.Models.Output; using Common; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.Json; namespace AssortGenerator { public class Program { static void Main(string[] args) { var assortsPath = CreateWorkingFolders(); InputFileHelper.SetInputFiles(assortsPath); // Get trader assort files from assorts input folder var traderAssortFilePaths = InputFileHelper.GetInputFilePaths().Where(x => TraderHelper.GetTraders().Values.Any(x.Contains)).ToList(); var finalisedQuestData = QuestHelper.GetFinalisedQuestData(); foreach (var trader in TraderHelper.GetTraders()) { // Get relevant trader dump var assortDumpPath = traderAssortFilePaths.Find(x => x.Contains($"getTraderAssort.{trader.Value}")); assortDumpPath ??= traderAssortFilePaths.Find(x => x.Contains($"{trader.Value}") && x.Contains("getTraderAssort")); // Convert input dump json into object var json = File.ReadAllText(assortDumpPath); var jsonObject = JsonDocument.Parse(json); // Find root data node var data = jsonObject.RootElement; // Find assort items node and parse into list string itemsJson = data.GetProperty("items").ToString(); List items = JsonSerializer.Deserialize>(itemsJson); // Fix items that have ran out of stock in the dump and give stack size FixZeroSizedStackAssorts(items, 100); FixFullyPurchasedStackLimits(items); // Find barter scheme node and parse into dictionary var barterSchemeJson = data.GetProperty("barter_scheme").ToString(); var barterSchemeItems = JsonSerializer.Deserialize>>>(barterSchemeJson); // Find loyalty level node and parse into dictionary var loyaltyLevelItemsJson = data.GetProperty("loyal_level_items").ToString(); var loyaltyLevelItems = JsonSerializer.Deserialize>(loyaltyLevelItemsJson); WriteOutputFilesForTrader(trader, items, barterSchemeItems, loyaltyLevelItems, finalisedQuestData); } JsonWriter.WriteJson(finalisedQuestData, "", Directory.GetCurrentDirectory(), "quests"); } private static void FixZeroSizedStackAssorts(List items, int defaultStackSize) { foreach (var item in items .Where(item => item.upd?.StackObjectsCount == 0 && item.slotId == "hideout")) { LoggingHelpers.LogError($"item {item._tpl} found with stack count of 0, changing to {defaultStackSize}"); if (item.upd?.BuyRestrictionMax != null) { var parsedRestrictionMax = int.Parse(item.upd?.BuyRestrictionMax.ToString()); if (parsedRestrictionMax > defaultStackSize) { item.upd.StackObjectsCount = parsedRestrictionMax; continue; } } item.upd.StackObjectsCount = defaultStackSize; } } private static void FixFullyPurchasedStackLimits(List items) { foreach (var item in items .Where(item => item.upd?.BuyRestrictionCurrent > 0 && item.slotId == "hideout")) { LoggingHelpers.LogError($"item {item._tpl} found with stack count > 0, changing to 0"); item.upd.BuyRestrictionCurrent = 0; } } /// /// Create input/assorts/output/traders folders /// private static string CreateWorkingFolders() { var workingPath = Directory.GetCurrentDirectory(); // create input folder var inputPath = $"{workingPath}//input"; DiskHelpers.CreateDirIfDoesntExist(inputPath); // create sub folder in input called assorts var assortsPath = $"{inputPath}//assorts"; DiskHelpers.CreateDirIfDoesntExist(assortsPath); // create output folder var outputPath = $"{workingPath}//output"; DiskHelpers.CreateDirIfDoesntExist(outputPath); // create traders sub-folder var tradersPath = $"{outputPath}//traders"; DiskHelpers.CreateDirIfDoesntExist(tradersPath); return assortsPath; } /// /// Parse raw tradersettings dump file into BaseRoot object /// /// list of file paths /// BaseRoot private static BaseRoot GetTraderData(IEnumerable filesInAssortsFolder) { var traderDataPath = filesInAssortsFolder.FirstOrDefault(x => x.Contains("resp.client.trading.api.traderSettings")); var traderDataJson = File.ReadAllText(traderDataPath); return JsonSerializer.Deserialize(traderDataJson); } private static void WriteOutputFilesForTrader( KeyValuePair trader, List items, Dictionary>> barterSchemeItems, Dictionary loyaltyLevelItems, Dictionary finalisedQuestData) { var workingPath = Directory.GetCurrentDirectory(); var traderData = GetTraderData(InputFileHelper.GetInputFilePaths()); var traderFolderPath = $"traders\\{trader.Value}"; // Create assort file, serialise into json and save into output folder var outputAssortFile = new AssortRoot { items = items, barter_scheme = barterSchemeItems, loyal_level_items = loyaltyLevelItems }; // create base file, serialise into json and save into output folder var outputBaseFile = traderData.data.Find(x => x._id == trader.Value); QuestAssort questAssort = GenerateQuestAssortForTrader(trader.Key, outputAssortFile, out List missingQuestAssorts); AttemptToAddMissingQuestAssorts(outputAssortFile, questAssort, missingQuestAssorts); JsonWriter.WriteJson(outputBaseFile, traderFolderPath, workingPath, "base"); JsonWriter.WriteJson(outputAssortFile, traderFolderPath, workingPath, "assort"); JsonWriter.WriteJson(questAssort, traderFolderPath, workingPath, "questassort"); //UpdateQuestAssortUnlockIds(trader.Value, questAssort, finalisedQuestData, outputAssortFile); // create suits file for ragman if (trader.Key == Trader.Ragman) { CreateRagmanSuitsFile(workingPath, traderFolderPath); } } private static void AttemptToAddMissingQuestAssorts(AssortRoot outputAssortFile, QuestAssort questAssort, List missingQuestAssorts) { // iterate over each missing assort foreach (var missingQuestAssort in missingQuestAssorts) { // Single item, maybe we can add one in (skip complex items like guns/ammo boxes etc) if (missingQuestAssort.Items.Count == 1) { // Check isn't a weapon var itemTemplate = ItemTemplateHelper.GetTemplateById(missingQuestAssort.Items[0]._tpl); if (itemTemplate._parent == "5447b5f14bdc2d61278b4567") { continue; } if (outputAssortFile.items.Any(x => x._id == missingQuestAssort.Items[0]._id)) { LoggingHelpers.LogError("OH NO, DUPE ID FOUND"); } // We can add it var item = new Item { _tpl = missingQuestAssort.Items[0]._tpl, _id = missingQuestAssort.Items[0]._id, parentId = "hideout", slotId = "hideout", upd = new Upd { StackObjectsCount = 10 } }; outputAssortFile.items.Add(item); var barterItemListInner = new List { // 10,000 Roubles new BarterObject() { _tpl = "5449016a4bdc2d6f028b456f", count = 25000 } }; var barterItemListOuter = new List> { barterItemListInner }; outputAssortFile.barter_scheme.Add(missingQuestAssort.Items[0]._id, barterItemListOuter); outputAssortFile.loyal_level_items[missingQuestAssort.Items[0]._id] = missingQuestAssort.LoyaltyLevel; var associatedQuestAssort = questAssort.success.FirstOrDefault(x => x.Value == missingQuestAssort.QuestId && x.Key.StartsWith("UnknownAssortId")); if (associatedQuestAssort.Key != null) { LoggingHelpers.LogWarning($"Able to replace missing quest: {missingQuestAssort.QuestId} assort with placeholder"); questAssort.success.Remove(associatedQuestAssort.Key); questAssort.success.Add(missingQuestAssort.Items[0]._id, missingQuestAssort.QuestId); } } } } private static void UpdateQuestAssortUnlockIds(string traderId, QuestAssort traderQuestAssort, Dictionary finalisedQuestData, AssortRoot traderAssortRoot) { var alreadyMatchedAssortIds = new List(); var assortUnlocks = QuestHelper.GetAssortUnlocks().Where(x => x.TraderId == traderId); foreach (var assort in assortUnlocks) { // Find quest that matches quest assort key Quest matchingQuest = finalisedQuestData.FirstOrDefault(x => x.Key == assort.QuestId).Value; if (matchingQuest == null) { continue; } if (matchingQuest.QuestName == "Debut") { var x = 2; } RewardStatus matchingReward = null; switch (assort.Criteria) { case "Success": matchingReward = matchingQuest.rewards.Success.Single(x => x.id == assort.AssortUnlockId); break; case "Started": matchingReward = matchingQuest.rewards.Started.Single(x => x.id == assort.AssortUnlockId); break; } // Try to find assort with an _id of the quests target value var matchingAssortInTrader = traderAssortRoot.items.SingleOrDefault(x => x._id == matchingReward.target); if (matchingAssortInTrader == null) { // Mismatch! quest reward has a target that doesnt exist in trader assort // Update to match quest Dictionary matching = null; switch (assort.Criteria) { case "Success": matching = traderQuestAssort.success; break; case "Started": matching = traderQuestAssort.started; break; } var kvp = matching.FirstOrDefault(x => x.Value == matchingQuest._id && !alreadyMatchedAssortIds.Contains(x.Value)); matchingReward.target = kvp.Key; // Quests can have multiple unlocks per quest, keep track of processed assort ids alreadyMatchedAssortIds.Add(kvp.Key); } } } /// /// merges bear and usec ragman clothing dumps into one file /// /// /// /// private static void CreateRagmanSuitsFile(string workingPath, string traderFolderPath) { var suitFileNames = new List(); suitFileNames.Add("usec.resp.client.trading.customization."); suitFileNames.Add("bear.resp.client.trading.customization."); var outputSuitData = new List(); foreach (var suitSideFilename in suitFileNames) { var customisationFilePath = InputFileHelper.GetInputFilePaths().FirstOrDefault(x => x.Contains(suitSideFilename)); if (string.IsNullOrEmpty(customisationFilePath)) { LoggingHelpers.LogWarning($"no suit file found: {suitSideFilename}, skipped"); continue; } var traderDataJson = File.ReadAllText(customisationFilePath); if (traderDataJson != null) { var suitData = JsonSerializer.Deserialize(traderDataJson); outputSuitData.AddRange(suitData.data); } } JsonWriter.WriteJson(outputSuitData, traderFolderPath, workingPath, "suits"); } /// /// Create quest assort file that links quest completions to trader assort unlocks /// /// /// /// private static QuestAssort GenerateQuestAssortForTrader(Trader trader, AssortRoot assortRoot, out List missingQuestAssorts) { missingQuestAssorts = new List(); var result = new QuestAssort(); var questData = QuestHelper.GetQuestData(); // Find assort unlocks List assortUnlocks = QuestHelper.GetAssortUnlocks(); // Store already matched items var matchedAssortItemIds = new List(); int unknownCount = 1; foreach (var assortUnlock in assortUnlocks.Where(x => x.TraderType == trader)) { if (assortUnlock.QuestId == "60e71ce009d7c801eb0c0ec6") { var x = 2; } // Get unlock item details var assortItemDetailsDB = ItemTemplateHelper.Items.FirstOrDefault(x => x.Key == assortUnlock.ItemUnlockedTemplateId); var ItemName = assortItemDetailsDB.Value._name; var assortItemModsFromQuest = assortUnlock.Items.Where(x => x.parentId == assortUnlock.ItemUnlockedId); // Get matching assorts from trader var results = GetMatchingTraderAssortsWithScore(assortRoot, assortUnlock, trader, matchedAssortItemIds); // No matches, add a placeholder to output file if (results.Keys.Count == 0) { // no assort found for this unlock, add to questassort with a placeholder value instead of assort id LoggingHelpers.LogError($"{trader} item templateId: {assortUnlock.ItemUnlockedTemplateId} ({ItemName}). questId: {assortUnlock.QuestId}. no assortId found"); result.success.Add($"UnknownAssortId{unknownCount}", assortUnlock.QuestId); unknownCount++; missingQuestAssorts.Add(assortUnlock); continue; } // All the assorts found have a matching score below 0 - very bad if (results.Values.All( x => x < 0)) { LoggingHelpers.LogError($"{trader} item templateId: {assortUnlock.ItemUnlockedTemplateId} ({ItemName}). questId: {assortUnlock.QuestId}. Only negative scored matches found"); result.success.Add($"UnknownAssortId{unknownCount}", assortUnlock.QuestId); unknownCount++; missingQuestAssorts.Add(assortUnlock); continue; } // get highest scoring match var highestScoringAssortIdMatch = results.OrderByDescending(x => x.Value).First().Key; // Add assort item id to blacklist so it wont be matched again matchedAssortItemIds.Add(highestScoringAssortIdMatch); if (result.success.ContainsKey(highestScoringAssortIdMatch)) { LoggingHelpers.LogWarning($"{trader} item templateId: {assortUnlock.ItemUnlockedTemplateId} ({ItemName}). questId: {assortUnlock.QuestId}. ALREADY EXISTS. SKIPPING"); continue; } LoggingHelpers.LogSuccess($"{trader} item templateId: {assortUnlock.ItemUnlockedTemplateId} ({ItemName}). questId: {assortUnlock.QuestId}. ADDING TO QUEST-ASSORT"); if (assortUnlock.Criteria == "Success") { result.success.Add(highestScoringAssortIdMatch, assortUnlock.QuestId); } else if (assortUnlock.Criteria == "Started") { result.started.Add(highestScoringAssortIdMatch, assortUnlock.QuestId); } else { LoggingHelpers.LogError($"{assortUnlock.Criteria} quest criteria not handled"); } //} //if (assortUnlock.Criteria.ToLower() == "fail") //{ // LoggingHelpers.LogError("Fail quest criteria not handled"); //} //if (assortUnlock.Criteria.ToLower() == "started") //{ // LoggingHelpers.LogError("started quest criteria not handled"); //} } return result; } /// /// Get a list of matching assorts with a score of how well they match the quest assort requirements /// /// Traders assort items/barter/loyalty /// /// /// /// private static Dictionary GetMatchingTraderAssortsWithScore( AssortRoot traderAssortRoot, AssortUnlocks assortUnlock, Trader trader, IEnumerable assortItemsThatMatchBlackList) { var quests = QuestHelper.GetFinalisedQuestData(); var assortItemDetailsDB = ItemTemplateHelper.Items.FirstOrDefault(x => x.Key == assortUnlock.ItemUnlockedTemplateId); var assortItemName = assortItemDetailsDB.Value._name; var assortItemModsFromQuest = assortUnlock.Items.Where(x => x.parentId == assortUnlock.ItemUnlockedId); // assort id + score var matchScores = new Dictionary(); List assortItemsThatMatch = traderAssortRoot.items .Where(x => x._tpl == assortUnlock.ItemUnlockedTemplateId && x.slotId == "hideout") .ToList(); if (assortItemsThatMatch?.Count == 0) { LoggingHelpers.LogError($"{trader} item templateId: {assortUnlock.ItemUnlockedTemplateId}. questId: {assortUnlock.QuestId}. No matches found. "); return matchScores; } if (assortItemsThatMatch?.Count > 2) { var questData = quests.FirstOrDefault(x => x.Key == assortUnlock.QuestId); LoggingHelpers.LogWarning($"{trader} item templateId: {assortUnlock.ItemUnlockedTemplateId}. questId: {questData.Value.QuestName}. multiple matches found. "); } foreach (var assortItemMatch in assortItemsThatMatch) { matchScores[assortItemMatch._id] = 0; var itemBarter = traderAssortRoot.barter_scheme[assortItemMatch._id]; var barterItems = itemBarter.First(); if (barterItems.Count == 1 && TplIsMoney(barterItems[0]._tpl)) { matchScores[assortItemMatch._id] += 2; } else { matchScores[assortItemMatch._id] += 1; } // Look up item in Loyalty Level array var associatedLoyaltyLevelItem = traderAssortRoot.loyal_level_items .FirstOrDefault(x => x.Key == assortItemMatch._id); if (associatedLoyaltyLevelItem.Value == assortUnlock.LoyaltyLevel) { // Loyalty level matches matchScores[assortItemMatch._id] += 5; } else { matchScores[assortItemMatch._id] -= 25; } if (assortItemsThatMatchBlackList.Contains(associatedLoyaltyLevelItem.Key)) { // Not the item we want, its already been matched matchScores[assortItemMatch._id] -= 25; } // Try matching by mods on item if they exist if (assortItemModsFromQuest.Any()) { var matchedItemMods = traderAssortRoot.items.Where(x => x.parentId == assortItemMatch._id).ToList(); if (assortItemModsFromQuest.Count() == matchedItemMods.Count) { // mod count from quest matches trader assort matchScores[assortItemMatch._id] += 10; } else { LoggingHelpers.LogWarning($"{trader} item templateId: {assortUnlock.ItemUnlockedTemplateId} ({assortItemName}). questId: {assortUnlock.QuestId}. mismatch of mod count, skipping"); continue; } foreach (var desiredMod in assortItemModsFromQuest) { // Compare each mod item in turn var matchingModInCurrentMatch = matchedItemMods.FirstOrDefault(x => x._tpl == desiredMod._tpl); if (matchingModInCurrentMatch != null) { matchScores[assortItemMatch._id] += 2; } else { LoggingHelpers.LogWarning($"{trader} item templateId: {assortUnlock.ItemUnlockedTemplateId} ({assortItemName}). questId: {assortUnlock.QuestId}. mismatch of mods, skipping"); matchScores[assortItemMatch._id] -= 2; } } } } return matchScores; } private static bool TplIsMoney(string tpl) { var moneyTpls = new string[] { "5449016a4bdc2d6f028b456f", "569668774bdc2da2298b4568", "5696686a4bdc2da3298b456a" }; return moneyTpls.Contains(tpl); } } }