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.IO; using System.Linq; using System.Text.Json; using static AssortGenerator.Common.Helpers.QuestHelper; 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(); var missingQuestAssortPrices = QuestHelper.GetMissingTraderQuestPrices(); 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 rootData = jsonObject.RootElement; var dataObject = rootData.GetProperty("data"); // Find assort items node and parse into list string itemsJson = dataObject.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); UpdateUnlimitedCountItemStackSize(items); FixFullyPurchasedStackLimits(items); // Find barter scheme node and parse into dictionary var barterSchemeJson = dataObject.GetProperty("barter_scheme").ToString(); var barterSchemeItems = JsonSerializer.Deserialize>>>(barterSchemeJson); foreach (var item in items.Where(item => item.upd?.BuyRestrictionMax is not null)) { item.upd.BuyRestrictionMax = int.Parse(item.upd.BuyRestrictionMax.ToString()); } // Find loyalty level node and parse into dictionary var loyaltyLevelItemsJson = dataObject.GetProperty("loyal_level_items").ToString(); var loyaltyLevelItems = JsonSerializer.Deserialize>(loyaltyLevelItemsJson); WriteOutputFilesForTrader(trader, items, barterSchemeItems, loyaltyLevelItems, finalisedQuestData, missingQuestAssortPrices); } JsonWriter.WriteJson(finalisedQuestData, "", Directory.GetCurrentDirectory(), "quests"); } private static void UpdateUnlimitedCountItemStackSize(List items) { foreach (var item in items .Where(item => (item.upd?.UnlimitedCount.GetValueOrDefault(false) ?? false) && item.slotId == "hideout")) { item.upd.StackObjectsCount = 9999999; } } 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 ((bool)(item.upd?.UnlimitedCount.GetValueOrDefault(false))) { // Handled elsewhere continue; } 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, Dictionary missingQuestAssortPrices ) { 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, missingQuestAssortPrices); 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, Dictionary missingQuestAssortPrices) { var missingAssortDataWithNoFix = new List(); var questData = QuestHelper.GetFinalisedQuestData(); // 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) var isSimpleItem = missingQuestAssort.Items.Count == 1; if (outputAssortFile.items.Any(x => x._id == missingQuestAssort.Items[0]._id)) { LoggingHelpers.LogError("OH NO, DUPE ID FOUND"); } // Find price data var hasPriceData = missingQuestAssortPrices.TryGetValue(missingQuestAssort.AssortUnlockId, out var priceData); if (isSimpleItem) { // We can add it var itemToAdd = new Item { _tpl = missingQuestAssort.Items[0]._tpl, _id = missingQuestAssort.Items[0]._id, parentId = "hideout", slotId = "hideout" }; // Set stack/buy max counts if (hasPriceData && priceData.itemTpl == missingQuestAssort.Items[0]._tpl) { itemToAdd.upd = priceData.itemUpd; } else { itemToAdd.upd = new Upd() { StackObjectsCount = 10 }; } var itemDefaultPreset = PresetHelper.GetDefaultPreset(missingQuestAssort.Items[0]._tpl); if (itemDefaultPreset != null) { // Add upd data to preset before we add to assort data itemDefaultPreset[0].upd = itemToAdd.upd; itemDefaultPreset[0].parentId = itemToAdd.parentId; itemDefaultPreset[0].slotId = itemToAdd.slotId; // Make ID match original data var badIdWeNeedToChange = itemDefaultPreset[0]._id; var goodId = missingQuestAssort.Items[0]._id; foreach (var presetItem in itemDefaultPreset) { if (presetItem._id == badIdWeNeedToChange) { // Update root id presetItem._id = goodId; continue; } if (presetItem.parentId == badIdWeNeedToChange) { presetItem.parentId = goodId; } } outputAssortFile.items.AddRange(itemDefaultPreset); } else { outputAssortFile.items.Add(itemToAdd); } } else { // multi-item assort! var itemsToAdd = ConvertRewardToItems(missingQuestAssort.Items); // Set stack/buy max counts if (hasPriceData && priceData.itemTpl == missingQuestAssort.Items[0]._tpl) { if (itemsToAdd.First().upd == null) { itemsToAdd.First().upd = new Upd(); } if (priceData.itemUpd.StackObjectsCount.HasValue) { itemsToAdd.First().upd.StackObjectsCount = priceData.itemUpd.StackObjectsCount.Value; } if (priceData.itemUpd.BuyRestrictionMax != null) { itemsToAdd.First().upd.BuyRestrictionMax = ((JsonElement)priceData.itemUpd.BuyRestrictionMax).GetInt32(); itemsToAdd.First().upd.BuyRestrictionCurrent = 0; } } else { var firstItem = itemsToAdd.First(); firstItem.upd ??= new Upd(); // no value in json, set stack count to 10 firstItem.upd.StackObjectsCount = 10; } outputAssortFile.items.AddRange(itemsToAdd); } var barterItemListInner = new List(); if (hasPriceData && priceData.itemTpl == missingQuestAssort.Items[0]._tpl) { barterItemListInner.AddRange(priceData.barterScheme); } else { // Default to 6969 roubles barterItemListInner.Add(new BarterObject() { _tpl = "5449016a4bdc2d6f028b456f", count = 6969 }); } 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} {questData.FirstOrDefault(x => x.Key == missingQuestAssort.QuestId).Value.QuestName}) assort with placeholder"); questAssort.success.Remove(associatedQuestAssort.Key); questAssort.success.Add(missingQuestAssort.Items[0]._id, missingQuestAssort.QuestId); } if (!hasPriceData) { missingAssortDataWithNoFix.Add(missingQuestAssort); } } var dict = new Dictionary(); foreach (var item in missingAssortDataWithNoFix) { dict.Add(item.AssortUnlockId, new { questName = questData.FirstOrDefault(x => x.Key == item.QuestId).Value.QuestName, questid = item.QuestId, itemTpl = item.ItemUnlockedTemplateId, itemUpd = new { BuyRestrictionMax = 0, StackObjectsCount = 100 }, barterScheme = new List { }, }); } JsonWriter.WriteJson(dict, "", Directory.GetCurrentDirectory(), "missingData"); } private static IEnumerable ConvertRewardToItems(List rewardToConvert) { var output = new List(); foreach (var rewardItem in rewardToConvert) { var item = new Item() { _id = rewardItem._id, _tpl = rewardItem._tpl, parentId = rewardItem.parentId ?? "hideout", slotId = rewardItem.slotId ?? "hideout" }; if (rewardItem.upd != null) { item.upd = (Upd)((JsonElement)rewardItem.upd).Deserialize(typeof(Upd)); } output.Add(item); } return output; } private static void UpdateQuestAssortUnlockIds(string traderId, QuestAssort traderQuestAssort, Dictionary finalisedQuestData, AssortRoot traderAssortRoot) { var alreadyMatchedAssortIds = new List(); var assortUnlocks = QuestHelper.GetAssortUnlocks(finalisedQuestData).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; } 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.GetFinalisedQuestData(); // Find assort unlocks List assortUnlocks = QuestHelper.GetAssortUnlocks(questData); // Store already matched items var matchedAssortItemIds = new List(); int unknownCount = 1; foreach (var assortUnlock in assortUnlocks.Where(x => x.TraderType == trader)) { // 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); } } }