667 lines
29 KiB
C#

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 data = jsonObject.RootElement;
// Find assort items node and parse into list
string itemsJson = data.GetProperty("items").ToString();
List<Item> items = JsonSerializer.Deserialize<List<Item>>(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 = data.GetProperty("barter_scheme").ToString();
var barterSchemeItems = JsonSerializer.Deserialize<Dictionary<string, List<List<BarterObject>>>>(barterSchemeJson);
// Find loyalty level node and parse into dictionary
var loyaltyLevelItemsJson = data.GetProperty("loyal_level_items").ToString();
var loyaltyLevelItems = JsonSerializer.Deserialize<Dictionary<string, int>>(loyaltyLevelItemsJson);
WriteOutputFilesForTrader(trader, items, barterSchemeItems, loyaltyLevelItems, finalisedQuestData, missingQuestAssortPrices);
}
JsonWriter.WriteJson(finalisedQuestData, "", Directory.GetCurrentDirectory(), "quests");
}
private static void UpdateunlimitedCountItemStackSize(List<Item> 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<Item> 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<Item> 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;
}
}
/// <summary>
/// Create input/assorts/output/traders folders
/// </summary>
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;
}
/// <summary>
/// Parse raw tradersettings dump file into BaseRoot object
/// </summary>
/// <param name="filesInAssortsFolder">list of file paths</param>
/// <returns>BaseRoot</returns>
private static BaseRoot GetTraderData(IEnumerable<string> filesInAssortsFolder)
{
var traderDataPath = filesInAssortsFolder.FirstOrDefault(x => x.Contains("resp.client.trading.api.traderSettings"));
var traderDataJson = File.ReadAllText(traderDataPath);
return JsonSerializer.Deserialize<BaseRoot>(traderDataJson);
}
private static void WriteOutputFilesForTrader(
KeyValuePair<Trader, string> trader,
List<Item> items,
Dictionary<string, List<List<BarterObject>>> barterSchemeItems,
Dictionary<string, int> loyaltyLevelItems,
Dictionary<string, Quest> finalisedQuestData,
Dictionary<string, MissingAssortPrice> 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<AssortUnlocks> 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<AssortUnlocks> missingQuestAssorts,
Dictionary<string, MissingAssortPrice> missingQuestAssortPrices)
{
var missingAssortDataWithNoFix = new List<AssortUnlocks>();
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<BarterObject>();
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<List<BarterObject>> { 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<string, object>();
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<string> { },
});
}
JsonWriter.WriteJson(dict, "", Directory.GetCurrentDirectory(), "missingData");
}
private static IEnumerable<Item> ConvertRewardToItems(List<QuestRewardItem> rewardToConvert)
{
var output = new List<Item>();
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<string, Quest> finalisedQuestData, AssortRoot traderAssortRoot)
{
var alreadyMatchedAssortIds = new List<string>();
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<string,string> 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);
}
}
}
/// <summary>
/// merges bear and usec ragman clothing dumps into one file
/// </summary>
/// <param name="trader"></param>
/// <param name="workingPath"></param>
/// <param name="traderFolderPath"></param>
private static void CreateRagmanSuitsFile(string workingPath, string traderFolderPath)
{
var suitFileNames = new List<string>();
suitFileNames.Add("usec.resp.client.trading.customization.");
suitFileNames.Add("bear.resp.client.trading.customization.");
var outputSuitData = new List<Suit>();
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<CustomisationRoot>(traderDataJson);
outputSuitData.AddRange(suitData.data);
}
}
JsonWriter.WriteJson(outputSuitData, traderFolderPath, workingPath, "suits");
}
/// <summary>
/// Create quest assort file that links quest completions to trader assort unlocks
/// </summary>
/// <param name="trader"></param>
/// <param name="assortRoot"></param>
/// <returns></returns>
private static QuestAssort GenerateQuestAssortForTrader(Trader trader, AssortRoot assortRoot, out List<AssortUnlocks> missingQuestAssorts)
{
missingQuestAssorts = new List<AssortUnlocks>();
var result = new QuestAssort();
var questData = QuestHelper.GetFinalisedQuestData();
// Find assort unlocks
List<AssortUnlocks> assortUnlocks = QuestHelper.GetAssortUnlocks(questData);
// Store already matched items
var matchedAssortItemIds = new List<string>();
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;
}
/// <summary>
/// Get a list of matching assorts with a score of how well they match the quest assort requirements
/// </summary>
/// <param name="traderAssortRoot">Traders assort items/barter/loyalty</param>
/// <param name="assortUnlock"></param>
/// <param name="trader"></param>
/// <param name="assortItemsThatMatchBlackList"></param>
/// <returns></returns>
private static Dictionary<string, int> GetMatchingTraderAssortsWithScore(
AssortRoot traderAssortRoot,
AssortUnlocks assortUnlock,
Trader trader,
IEnumerable<string> 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<string, int>();
List<Item> 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);
}
}
}