From 51fc2c4a0bb319d614fa6fc6d1b4f0c8fd3a054d Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 17 Jun 2025 09:50:32 +0100 Subject: [PATCH] Created `GetItemsToRetrievePool` to handle logic instead of using `GetRewardableItems`, now returns tpls instead of entire objects #401 Decoupled `IsValidRewardItem` from repeatable config Made `IsValidRewardItem` public Added `RequiredItemTypeBlacklist` to Completion config, Blacklisted ammo More nullguards --- .../SPT_Data/configs/quest.json | 6 +- .../Generators/RepeatableQuestGenerator.cs | 115 +++++++++++------- .../RepeatableQuestRewardGenerator.cs | 16 ++- .../Models/Spt/Config/QuestConfig.cs | 10 ++ 4 files changed, 95 insertions(+), 52 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json index 43b7b3d7..8d0ffbd6 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json @@ -330,7 +330,6 @@ "5485a8684bdc2da71d8b4567", "55818b224bdc2dde698b456f", "57864a66245977548f04a81f", - "55818af64bdc2d5b648b4570", "550aa4dd4bdc2dc9348b4569", "55818a594bdc2db9688b456a", "55818a104bdc2db9688b4569" @@ -1216,7 +1215,10 @@ "requiredItemMinDurabilityMinMax": { "min": 60, "max": 80 - } + }, + "requiredItemTypeBlacklist": [ + "5485a8684bdc2da71d8b4567" + ] }, "Elimination": [ { diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGenerator.cs index 51596ea4..6b7bc980 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGenerator.cs @@ -28,6 +28,7 @@ public class RepeatableQuestGenerator( DatabaseService _databaseService, LocalisationService _localisationService, ConfigServer _configServer, + SeasonalEventService _seasonalEventService, ICloner _cloner ) { @@ -557,25 +558,27 @@ public class RepeatableQuestGenerator( RepeatableQuestConfig repeatableConfig ) { - var completionConfig = repeatableConfig.QuestConfig.Completion; + var completionConfig = repeatableConfig?.QuestConfig?.Completion; + if (completionConfig is null) + { + _logger.Error("Unable to generate Completion quest, no Completion config found"); + return null; + } var levelsConfig = repeatableConfig.RewardScaling.Levels; var roublesConfig = repeatableConfig.RewardScaling.Roubles; var quest = GenerateRepeatableTemplate("Completion", traderId, repeatableConfig.Side, sessionId); - // Filter the items.json items to items the player must retrieve to complete quest: shouldn't be a quest item or "non-existant" - var possibleItemsToRetrievePool = _repeatableQuestRewardGenerator.GetRewardableItems( - repeatableConfig, - traderId - ); + // Filter the items.json items to items the player must retrieve to complete quest: shouldn't be a quest item or "non-existent" + var itemsToRetrievePool = GetItemsToRetrievePool(completionConfig, repeatableConfig.RewardBlacklist); - // Be fair, don't var the items be more expensive than the reward - var multi = _randomUtil.GetDouble(0.5, 1); + // Be fair, don't value the items be more expensive than the reward + var multiplier = _randomUtil.GetDouble(0.5, 1); var roublesBudget = Math.Floor( - (double) (_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multi) + (double) (_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multiplier) ); roublesBudget = Math.Max(roublesBudget, 5000d); - var itemSelection = possibleItemsToRetrievePool.Where(x => _itemHelper.GetItemPrice(x.Id) < roublesBudget + var itemSelection = itemsToRetrievePool.Where(itemTpl => _itemHelper.GetItemPrice(itemTpl) < roublesBudget ) .ToList(); @@ -585,7 +588,7 @@ public class RepeatableQuestGenerator( { var itemWhitelist = _databaseService.GetTemplates().RepeatableQuests.Data.Completion.ItemsWhitelist; - // Filter and concatenate the arrays according to current player level + // Filter and concatenate items according to current player level var itemIdsWhitelisted = itemWhitelist .Where(p => p.MinPlayerLevel <= pmcLevel) .SelectMany(x => x.ItemIds) @@ -593,8 +596,8 @@ public class RepeatableQuestGenerator( itemSelection = itemSelection.Where(x => { // Whitelist can contain item tpls and item base type ids - return itemIdsWhitelisted.Any(v => _itemHelper.IsOfBaseclass(x.Id, v)) || - itemIdsWhitelisted.Contains(x.Id); + return itemIdsWhitelisted.Any(v => _itemHelper.IsOfBaseclass(x, v)) || + itemIdsWhitelisted.Contains(x); } ) .ToList(); @@ -607,7 +610,7 @@ public class RepeatableQuestGenerator( { var itemBlacklist = _databaseService.GetTemplates().RepeatableQuests.Data.Completion.ItemsBlacklist; - // we filter and concatenate the arrays according to current player level + // Filter and concatenate the arrays according to current player level var itemIdsBlacklisted = itemBlacklist .Where(p => p.MinPlayerLevel <= pmcLevel) .SelectMany(x => x.ItemIds) @@ -615,13 +618,14 @@ public class RepeatableQuestGenerator( itemSelection = itemSelection.Where(x => { - return itemIdsBlacklisted.All(v => !_itemHelper.IsOfBaseclass(x.Id, v)) || - !itemIdsBlacklisted.Contains(x.Id); + return itemIdsBlacklisted.All(v => !_itemHelper.IsOfBaseclass(x, v)) || + !itemIdsBlacklisted.Contains(x); } ) .ToList(); } + // Filtering too harsh if (!itemSelection.Any()) { _logger.Error( @@ -633,10 +637,7 @@ public class RepeatableQuestGenerator( return null; } - // Draw items to ask player to retrieve - var isAmmo = 0; - - // Store the indexes of items we are asking player to provide + // Store the indexes of items we are asking player to supply var distinctItemsToRetrieveCount = _randomUtil.GetInt(1, completionConfig.UniqueItemCount.Value); var chosenRequirementItemsTpls = new List(); var usedItemIndexes = new HashSet(); @@ -674,32 +675,18 @@ public class RepeatableQuestGenerator( return null; } + // Store index of item we've already chosen for later checking usedItemIndexes.Add(chosenItemIndex); - var itemSelected = itemSelection[chosenItemIndex]; - var itemUnitPrice = _itemHelper.GetItemPrice(itemSelected.Id).Value; + var tplChosen = itemSelection[chosenItemIndex]; + var itemPrice = _itemHelper.GetItemPrice(tplChosen).Value; var minValue = completionConfig.MinimumRequestedAmount.Value; var maxValue = completionConfig.MaximumRequestedAmount.Value; - if (_itemHelper.IsOfBaseclass(itemSelected.Id, BaseClasses.AMMO)) - { - // Prevent multiple ammo requirements from being picked - if (isAmmo > 0 && isAmmo < _maxRandomNumberAttempts) - { - isAmmo++; - i--; - - continue; - } - - isAmmo++; - minValue = completionConfig.MinimumRequestedBulletAmount.Value; - maxValue = completionConfig.MaximumRequestedBulletAmount.Value; - } var value = minValue; // Get the value range within budget - var x = (int) Math.Floor(roublesBudget / itemUnitPrice); + var x = (int) Math.Floor(roublesBudget / itemPrice); maxValue = Math.Min(maxValue, x); if (maxValue > minValue) // If it doesn't blow the budget we have for the request, draw a random amount of the selected @@ -708,19 +695,21 @@ public class RepeatableQuestGenerator( value = _randomUtil.RandInt(minValue, maxValue + 1); } - roublesBudget -= value * itemUnitPrice; + roublesBudget -= value * itemPrice; - // Push a CompletionCondition with the item and the amount of the item - chosenRequirementItemsTpls.Add(itemSelected.Id); - quest.Conditions.AvailableForFinish.Add(GenerateCompletionAvailableForFinish(itemSelected.Id, value, repeatableConfig.QuestConfig.Completion)); + // Push a CompletionCondition with the item and the amount of the item into quest + chosenRequirementItemsTpls.Add(tplChosen); + quest.Conditions.AvailableForFinish.Add(GenerateCompletionAvailableForFinish(tplChosen, value, repeatableConfig.QuestConfig.Completion)); + // Is there budget left for more items if (roublesBudget > 0) { - // Reduce the list possible items to fulfill the new budget constraint - itemSelection = itemSelection.Where(dbItem => _itemHelper.GetItemPrice(dbItem.Id) < roublesBudget) + // Reduce item pool to fit budget + itemSelection = itemSelection.Where(tpl => _itemHelper.GetItemPrice(tpl) < roublesBudget) .ToList(); if (!itemSelection.Any()) { + // Nothing fits new budget, exit break; } } @@ -742,6 +731,44 @@ public class RepeatableQuestGenerator( return quest; } + /// + /// Generate a pool of item tpls the player should reasonably be able to retrieve + /// + /// Completion quest type config + /// Item tpls to not add to pool + /// Set of item tpls + protected HashSet GetItemsToRetrievePool(Completion completionConfig, HashSet itemTplBlacklist) + { + // Get seasonal items that should not be added to pool as seasonal event is not active + var seasonalItems = _seasonalEventService.GetInactiveSeasonalEventItems(); + + // Check for specific base classes which don't make sense as reward item + // also check if the price is greater than 0; there are some items whose price can not be found + return _databaseService.GetItems() + .Values.Where(itemTemplate => + { + // Base "Item" item has no parent, ignore it + if (itemTemplate.Parent == string.Empty) + { + return false; + } + + if (seasonalItems.Contains(itemTemplate.Id)) + { + return false; + } + + // Valid reward items share same logic as items to retrieve + return _repeatableQuestRewardGenerator.IsValidRewardItem( + itemTemplate.Id, + itemTplBlacklist, + completionConfig.RequiredItemTypeBlacklist + ); + } + ).Select(item => item.Id) + .ToHashSet(); + } + /// /// A repeatable quest, besides some more or less static components, exists of reward and condition (see /// assets/database/templates/repeatableQuests.json) diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestRewardGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestRewardGenerator.cs index 7374d809..4c72752a 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestRewardGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestRewardGenerator.cs @@ -693,7 +693,7 @@ public class RepeatableQuestRewardGenerator( // Get an array of seasonal items that should not be shown right now as seasonal event is not active var seasonalItems = _seasonalEventService.GetInactiveSeasonalEventItems(); - // Check for specific baseclasses which don't make sense as reward item + // Check for specific base classes which don't make sense as reward item // also check if the price is greater than 0; there are some items whose price can not be found // those are not in the game yet (e.g. AGS grenade launcher) return _databaseService.GetItems() @@ -715,7 +715,8 @@ public class RepeatableQuestRewardGenerator( return IsValidRewardItem( itemTemplate.Id, - repeatableQuestConfig, + repeatableQuestConfig.RewardBlacklist, + repeatableQuestConfig.RewardBaseTypeBlacklist, traderWhitelist?.RewardBaseWhitelist ); } @@ -728,10 +729,13 @@ public class RepeatableQuestRewardGenerator( /// or content of bot loot. Items that are tested as valid may be in a player backpack or stash. /// /// Template id of item to check - /// Config + /// Specific item tpls to ignore + /// Specific item base types to ignore /// Default null, specific trader item base classes /// True if item is valid reward - protected bool IsValidRewardItem(string tpl, RepeatableQuestConfig repeatableQuestConfig, + public bool IsValidRewardItem(string tpl, + HashSet itemTplBlacklist, + HashSet itemTypeBlacklist, List? itemBaseWhitelist = null) { // Return early if not valid item to give as reward @@ -744,7 +748,7 @@ public class RepeatableQuestRewardGenerator( if ( _itemFilterService.IsItemBlacklisted(tpl) || _itemFilterService.IsItemRewardBlacklisted(tpl) || - repeatableQuestConfig.RewardBlacklist.Contains(tpl) || + itemTplBlacklist.Contains(tpl) || _itemFilterService.IsItemBlacklisted(tpl) ) { @@ -752,7 +756,7 @@ public class RepeatableQuestRewardGenerator( } // Item has blacklisted base types - if (_itemHelper.IsOfBaseclasses(tpl, repeatableQuestConfig.RewardBaseTypeBlacklist)) + if (_itemHelper.IsOfBaseclasses(tpl, itemTypeBlacklist)) { return false; } diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs index a39a57fd..0164831f 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs @@ -600,6 +600,16 @@ public record Completion : BaseQuestConfig get; set; } + + /// + /// Blacklisted item types to not collect + /// + [JsonPropertyName("requiredItemTypeBlacklist")] + public HashSet? RequiredItemTypeBlacklist + { + get; + set; + } } public record Pickup : BaseQuestConfig