From 3f41db4b9ba7167997045490aa054e048b7cf3c8 Mon Sep 17 00:00:00 2001 From: Chomp Date: Wed, 5 Feb 2025 10:59:18 +0000 Subject: [PATCH] Added `HideoutCraftQuestIdGenerator` project --- .../HideoutCraftQuestIdGenerator.cs | 220 ++++++++++++++++++ .../HideoutCraftQuestIdGenerator.csproj | 19 ++ .../HideoutCraftQuestIdGeneratorLauncher.cs | 28 +++ .../SptBasicLogger.cs | 62 +++++ Tools/ItemTplGenerator/ItemTplGenerator.cs | 6 +- server-csharp.sln | 12 +- 6 files changed, 344 insertions(+), 3 deletions(-) create mode 100644 Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGenerator.cs create mode 100644 Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGenerator.csproj create mode 100644 Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGeneratorLauncher.cs create mode 100644 Tools/HideoutCraftQuestIdGenerator/SptBasicLogger.cs diff --git a/Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGenerator.cs b/Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGenerator.cs new file mode 100644 index 00000000..ff65929f --- /dev/null +++ b/Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGenerator.cs @@ -0,0 +1,220 @@ +using Core.Callbacks; +using Core.DI; +using Core.Helpers; +using Core.Models.Eft.Common.Tables; +using Core.Models.Eft.Hideout; +using Core.Models.Enums; +using Core.Models.Utils; +using Core.Servers; +using Core.Services; +using Core.Utils; +using SptCommon.Annotations; +using Path = System.IO.Path; + +namespace HideoutCraftQuestIdGenerator; + +[Injectable] +public class HideoutCraftQuestIdGenerator( + ISptLogger _logger, + FileUtil _fileUtil, + JsonUtil _jsonUtil, + DatabaseServer _databaseServer, + LocaleService _localeService, + ItemHelper _itemHelper, + IEnumerable _onLoadComponents +) +{ + private readonly List _questProductionOutputList = []; + private readonly Dictionary _questProductionMap = new(); + + private readonly HashSet _blacklistedProductions = + [ + "6617cdb6b24b0ea24505f618", // Old event quest production "Radio Repeater" alt recipe + "66140c4a9688754de10dac07", // Old event quest production "Documents with decrypted data" + "661e6c26750e453380391f55", // Old event quest production "Documents with decrypted data" + "660c2dbaa2a92e70cc074863", // Old event quest production "Decrypted flash drive" + "67093210d514d26f8408612b" // Old event quest production "TG-Vi-24 true vaccine" + ]; + + private readonly Dictionary _forcedQuestToProductionAssociations = new() + { + // KEY = PRODUCTION, VALUE = QUEST + { "63a571802116d261d2336cd1", "625d6ffaf7308432be1d44c5" } // Network Provider - Part 2 + }; + + public async Task Run() + { + // We only need the DB for this, other OnLoad events alter the data + var dbOnload = _onLoadComponents.FirstOrDefault(x => x.GetRoute() == "spt-database"); + await dbOnload.OnLoad(); + + // Build up our dataset + BuildQuestProductionList(); + UpdateProductionQuests(); + + // Figure out our source and target directories + var projectDir = Directory.GetParent("./").Parent.Parent.Parent.Parent.Parent; + var productionPath = "Libraries\\SptAssets\\Assets\\database\\hideout\\production.json"; + var productionFilePath = Path.Combine(projectDir.FullName, productionPath); + + var updatedProductionJson = _jsonUtil.Serialize(_databaseServer.GetTables().Hideout.Production, true); + _fileUtil.WriteFile(productionFilePath, updatedProductionJson); + } + + // Build a list of all quests and what production they unlock + private void BuildQuestProductionList() + { + foreach (var (questId, quest) in _databaseServer.GetTables().Templates.Quests) + { + var combinedRewards = CombineRewards(quest.Rewards).Where(x => x.Type == RewardType.ProductionScheme).ToList(); + foreach (var reward in combinedRewards) + { + // Assume all productions only output a single item template + var output = new QuestProductionOutput + { + QuestId = questId, + ItemTemplate = reward.Items[0].Template, + Quantity = 0 + }; + + // Loop over root items only, ignore children + foreach (var item in reward.Items.Where(x => x.ParentId is null)) + { + if (item.Template != output.ItemTemplate) + { + _logger.Error( + $"Production scheme has multiple output items. " + + $"{output.ItemTemplate} != {item.Template}" + ); + + continue; + } + + output.Quantity += item.Upd.StackObjectsCount.Value; + } + + _questProductionOutputList.Add(output); + } + } + } + + private void UpdateProductionQuests() + { + // Loop through all productions, and try to associate any with a `QuestComplete` type with its quest + foreach (var production in _databaseServer.GetTables().Hideout.Production.Recipes) + { + // Skip blacklisted productions + if (_blacklistedProductions.Contains(production.Id)) + { + continue; + } + + // Look for a 'quest completion' requirement + var questCompleteRequirements = production.Requirements.Where(req => req.Type == "QuestComplete").ToList(); + if (questCompleteRequirements.Count == 0) + { + // Production has no quest requirement + continue; + } + + if (questCompleteRequirements.Count > 1) + { + _logger.Error($"Error, production: {production.Id} contains multiple QuestComplete requirements"); + + // Production has no multiple quest requirements + continue; + } + + // Check for forced ids + if (_forcedQuestToProductionAssociations.TryGetValue(production.Id, out var associatedQuestIdToComplete)) + { + // Found one, move to next production + _logger.Success($"FORCED - Updated: {production.Id} {production.EndProduct} ({_itemHelper.GetItemName(production.EndProduct)}) with quantity: {production.Count} to target quest: {associatedQuestIdToComplete}" + ); + questCompleteRequirements[0].QuestId = associatedQuestIdToComplete; + + continue; + } + + // Try to find the quest that matches this production + var questProductionOutputs = _questProductionOutputList.Where( + output => output.ItemTemplate == production.EndProduct && output.Quantity == production.Count + ) + .ToList(); + + // Make sure we found valid data + if (!IsValidQuestProduction(production, questProductionOutputs, questCompleteRequirements[0])) + { + continue; + } + + // Update the production quest ID + _questProductionMap[questProductionOutputs[0].QuestId] = production.Id; + questCompleteRequirements[0].QuestId = questProductionOutputs[0].QuestId; + _logger.Success( + $"Updated: {production.Id}, {production.EndProduct} with quantity: {production.Count} to target quest: {questProductionOutputs[0].QuestId}" + ); + } + } + + private bool IsValidQuestProduction(HideoutProduction production, + List questProductionOutputs, Requirement questComplete) + { + // A lot of error handling for edge cases + if (!questProductionOutputs.Any()) + { + _logger.Error( + $"Unable to find quest for production: {production.Id}, endProduct: {production.EndProduct} ({_itemHelper.GetItemName(production.EndProduct)}) with quantity: {production.Count}. Potential new or removed quest" + ); + return false; + } + + if (questProductionOutputs.Count > 1) + { + _logger.Error( + $"Multiple quests match production {production.Id}, endProduct {production.EndProduct} with quantity: {production.Count}" + ); + return false; + } + + if (questComplete.QuestId is not null && questComplete.QuestId != questProductionOutputs[0].QuestId) + { + _logger.Error( + $"Multiple productions match quest.EndProduct {production.EndProduct} with quantity {production.Count}, existing quest: {questComplete.QuestId}" + ); + + return false; + } + + if (_questProductionMap.ContainsKey(questProductionOutputs[0].QuestId)) + { + _logger.Warning( + $"Quest {questProductionOutputs[0].QuestId} is already associated with production: {_questProductionMap[questProductionOutputs[0].QuestId]}. Potential conflict" + ); + } + + return true; + } + + private HashSet CombineRewards(QuestRewards? questRewards) + { + var result = new HashSet(); + questRewards.Started?.ForEach(x => result.Add(x)); + questRewards.Success?.ForEach(x => result.Add(x)); + questRewards.AvailableForFinish?.ForEach(x => result.Add(x)); + questRewards.Expired?.ForEach(x => result.Add(x)); + questRewards.AvailableForStart?.ForEach(x => result.Add(x)); + questRewards.Fail?.ForEach(x => result.Add(x)); + questRewards.FailRestartable?.ForEach(x => result.Add(x)); + questRewards.Started?.ForEach(x => result.Add(x)); + + return result; + } +} + +public class QuestProductionOutput +{ + public string QuestId { get; set; } + public string ItemTemplate { get; set; } + public double Quantity { get; set; } +} diff --git a/Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGenerator.csproj b/Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGenerator.csproj new file mode 100644 index 00000000..363a48df --- /dev/null +++ b/Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGenerator.csproj @@ -0,0 +1,19 @@ + + + + true + false + Exe + net9.0 + enable + enable + + + + + + + + + + diff --git a/Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGeneratorLauncher.cs b/Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGeneratorLauncher.cs new file mode 100644 index 00000000..2788222b --- /dev/null +++ b/Tools/HideoutCraftQuestIdGenerator/HideoutCraftQuestIdGeneratorLauncher.cs @@ -0,0 +1,28 @@ +using Core.Utils; +using Microsoft.Extensions.DependencyInjection; +using SptDependencyInjection; + +namespace HideoutCraftQuestIdGenerator; + +public class HideoutCraftQuestIdGeneratorLauncher +{ + public static void Main(string[] args) + { + try + { + var serviceCollection = new ServiceCollection(); + DependencyInjectionRegistrator.RegisterSptComponents( + typeof(HideoutCraftQuestIdGeneratorLauncher).Assembly, + typeof(App).Assembly, + serviceCollection + ); + var serviceProvider = serviceCollection.BuildServiceProvider(); + serviceProvider.GetService().Run().Wait(); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } +} diff --git a/Tools/HideoutCraftQuestIdGenerator/SptBasicLogger.cs b/Tools/HideoutCraftQuestIdGenerator/SptBasicLogger.cs new file mode 100644 index 00000000..665d85cf --- /dev/null +++ b/Tools/HideoutCraftQuestIdGenerator/SptBasicLogger.cs @@ -0,0 +1,62 @@ +using Core.Models.Logging; +using Core.Models.Spt.Logging; +using Core.Models.Utils; +using SptCommon.Annotations; + +namespace HideoutCraftQuestIdGenerator; + +[Injectable] +public class SptBasicLogger : ISptLogger +{ + private readonly string categoryName; + public SptBasicLogger() + { + categoryName = typeof(T).Name; + } + + public void LogWithColor(string data, LogTextColor? textColor = null, LogBackgroundColor? backgroundColor = null, + Exception? ex = null) + { + Console.WriteLine($"{categoryName}: {data}"); + } + + public void Success(string data, Exception? ex = null) + { + Console.WriteLine($"{categoryName}: {data}"); + } + + public void Error(string data, Exception? ex = null) + { + Console.WriteLine($"{categoryName}: {data}"); + } + + public void Warning(string data, Exception? ex = null) + { + Console.WriteLine($"{categoryName}: {data}"); + } + + public void Info(string data, Exception? ex = null) + { + Console.WriteLine($"{categoryName}: {data}"); + } + + public void Debug(string data, Exception? ex = null) + { + Console.WriteLine($"{categoryName}: {data}"); + } + + public void Critical(string data, Exception? ex = null) + { + Console.WriteLine($"{categoryName}: {data}"); + } + + public void WriteToLogFile(string body) + { + Console.WriteLine($"{categoryName}: {body}"); + } + + public bool IsLogEnabled(LogLevel level) + { + return true; + } +} diff --git a/Tools/ItemTplGenerator/ItemTplGenerator.cs b/Tools/ItemTplGenerator/ItemTplGenerator.cs index 648d5462..f13c66df 100644 --- a/Tools/ItemTplGenerator/ItemTplGenerator.cs +++ b/Tools/ItemTplGenerator/ItemTplGenerator.cs @@ -6,6 +6,7 @@ using Core.Models.Enums; using Core.Models.Utils; using Core.Servers; using Core.Services; +using Core.Utils; using SptCommon.Annotations; using SptCommon.Extensions; using Path = System.IO.Path; @@ -18,7 +19,7 @@ public class ItemTplGenerator( DatabaseServer _databaseServer, LocaleService _localeService, ItemHelper _itemHelper, - // @inject("FileSystemSync") protected fileSystemSync: FileSystemSync, + FileUtil _fileUtil, IEnumerable _onLoadComponents ) { @@ -30,7 +31,7 @@ public class ItemTplGenerator( public async Task Run() { itemOverrides = ItemOverrides.ItemOverridesDictionary; - // Load all of the onload components, this gives us access to most of SPTs injections + // Load all onload components, this gives us access to most of SPTs injections foreach (var onLoad in _onLoadComponents) { if (onLoad is HttpCallbacks) @@ -563,6 +564,7 @@ public class ItemTplGenerator( } // TODO: enable once we dont get any more errors + throw new NotImplementedException(); // this.fileSystemSync.write(outputPath, enumFileData); } } diff --git a/server-csharp.sln b/server-csharp.sln index 2f8e5249..060cc192 100644 --- a/server-csharp.sln +++ b/server-csharp.sln @@ -1,5 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35527.113 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{1F5ED9C6-8B1F-4776-85AB-B387CBBC5557}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Libraries\Core\Core.csproj", "{AC8643DC-8779-4B4A-BBDA-2D4CC466F765}" @@ -20,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SptCommon", "SptCommon\SptC EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SptAssets", "Libraries\SptAssets\SptAssets.csproj", "{4B973AC0-0C60-4853-9AF7-7CB69127473E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HideoutCraftQuestIdGenerator", "Tools\HideoutCraftQuestIdGenerator\HideoutCraftQuestIdGenerator.csproj", "{C24B1FEB-F8AC-434E-998D-5DA4D1687295}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,15 +63,20 @@ Global {4B973AC0-0C60-4853-9AF7-7CB69127473E}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B973AC0-0C60-4853-9AF7-7CB69127473E}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B973AC0-0C60-4853-9AF7-7CB69127473E}.Release|Any CPU.Build.0 = Release|Any CPU + {C24B1FEB-F8AC-434E-998D-5DA4D1687295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C24B1FEB-F8AC-434E-998D-5DA4D1687295}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C24B1FEB-F8AC-434E-998D-5DA4D1687295}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C24B1FEB-F8AC-434E-998D-5DA4D1687295}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {AC8643DC-8779-4B4A-BBDA-2D4CC466F765} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF} {4B4AF50D-B2C6-47D1-B567-EA4560D8CBA1} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF} {00897F10-1AB3-4DC7-8DF9-5EA1D0289ACF} = {587959C2-5AFA-4B77-B327-566610F9A289} - {AC8643DC-8779-4B4A-BBDA-2D4CC466F765} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF} {DB049C81-DEC0-490D-AC06-7AF4DC8C0571} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF} {4B973AC0-0C60-4853-9AF7-7CB69127473E} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF} + {C24B1FEB-F8AC-434E-998D-5DA4D1687295} = {587959C2-5AFA-4B77-B327-566610F9A289} EndGlobalSection EndGlobal