From b3dca61ac07432a9ec25caa30b387d96fa3f5061 Mon Sep 17 00:00:00 2001 From: Cj <161484149+CJ-SPT@users.noreply.github.com> Date: Mon, 23 Jun 2025 05:03:56 -0400 Subject: [PATCH] Break rest of repeatable quest generation code into components. Fix nullability of exploration generation and improve error handling, make new helper method, add pick random quest type method to controller (#419) --- .../SPT_Data/configs/quest.json | 6 +- .../Controllers/RepeatableQuestController.cs | 98 +++- .../CompletionQuestGenerator.cs | 7 +- .../EliminationQuestGenerator.cs} | 422 +++--------------- .../ExplorationQuestGenerator.cs | 319 +++++++++++++ .../IRepeatableQuestGenerator.cs | 16 + .../PickupQuestGenerator.cs | 87 ++++ .../Helpers/RepeatableQuestHelper.cs | 21 +- .../Models/Spt/Config/QuestConfig.cs | 4 +- .../Models/Spt/Repeatable/QuestTypePool.cs | 10 +- 10 files changed, 600 insertions(+), 390 deletions(-) rename Libraries/SPTarkov.Server.Core/Generators/{RepeatableQuestGenerator.cs => RepeatableQuestGeneration/EliminationQuestGenerator.cs} (56%) create mode 100644 Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs create mode 100644 Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/IRepeatableQuestGenerator.cs create mode 100644 Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/PickupQuestGenerator.cs diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json index ecec9e90..16a82bfa 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json @@ -395,7 +395,7 @@ "maxExtractsWithSpecificExit": 2, "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "specificExits": { - "probability": 0.2, + "chance": 20, "passageRequirementWhitelist": [ "None", "TransferItem", @@ -1194,7 +1194,7 @@ "maxExtractsWithSpecificExit": 4, "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "specificExits": { - "probability": 0.1, + "chance": 10, "passageRequirementWhitelist": [ "None", "TransferItem", @@ -1947,7 +1947,7 @@ "maxExtracts": 3, "maxExtractsWithSpecificExit": 1, "specificExits": { - "probability": 0.2, + "chance": 20, "passageRequirementWhitelist": ["None", "WorldEvent", "Train", "Reference", "Empty"] } }, diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs index 4ad551ae..c7fb19d7 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs @@ -1,5 +1,6 @@ using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Generators; +using SPTarkov.Server.Core.Generators.RepeatableQuestGeneration; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; @@ -24,6 +25,10 @@ namespace SPTarkov.Server.Core.Controllers; [Injectable] public class RepeatableQuestController( ISptLogger _logger, + EliminationQuestGenerator _eliminationQuestGenerator, + CompletionQuestGenerator _completionQuestGenerator, + ExplorationQuestGenerator _explorationQuestGenerator, + PickupQuestGenerator _pickupQuestGenerator, TimeUtil _timeUtil, MathUtil _mathUtil, RandomUtil _randomUtil, @@ -33,7 +38,6 @@ public class RepeatableQuestController( LocalisationService _localisationService, EventOutputHolder _eventOutputHolder, PaymentService _paymentService, - RepeatableQuestGenerator _repeatableQuestGenerator, RepeatableQuestHelper _repeatableQuestHelper, QuestHelper _questHelper, DatabaseService _databaseService, @@ -42,7 +46,7 @@ public class RepeatableQuestController( ) { protected static readonly List _questTypes = ["PickUp", "Exploration", "Elimination"]; - protected QuestConfig _questConfig = _configServer.GetConfig(); + protected QuestConfig QuestConfig = _configServer.GetConfig(); /// /// Handle the client accepting a repeatable quest and starting it @@ -152,7 +156,7 @@ public class RepeatableQuestController( repeatablesOfTypeInProfile.ChangeRequirement.Remove(changeRequest.QuestId); // Get config for this repeatable subtype (daily/weekly/scav) - var repeatableConfig = _questConfig.RepeatableQuests.FirstOrDefault(config => + var repeatableConfig = QuestConfig.RepeatableQuests.FirstOrDefault(config => config.Name == repeatablesOfTypeInProfile.Name ); @@ -374,7 +378,7 @@ public class RepeatableQuestController( var attempts = 0; while (attempts < maxAttempts && questTypePool.Types.Count > 0) { - newRepeatableQuest = _repeatableQuestGenerator.GenerateRepeatableQuest( + newRepeatableQuest = PickAndGenerateRandomRepeatableQuest( sessionId, pmcData.Info.Level.Value, pmcData.TradersInfo, @@ -404,6 +408,86 @@ public class RepeatableQuestController( return newRepeatableQuest; } + /// + /// This method is called by /GetClientRepeatableQuests/ and creates one element of quest type format (see + /// assets/database/templates/repeatableQuests.json). + /// It randomly draws a quest type (currently Elimination, Completion or Exploration) as well as a trader who is + /// providing the quest + /// + /// Session id + /// Player's level for requested items and reward generation + /// Players trader standing/rep levels + /// Possible quest types pool + /// Repeatable quest config + /// RepeatableQuest + public RepeatableQuest? PickAndGenerateRandomRepeatableQuest( + string sessionId, + int pmcLevel, + Dictionary pmcTraderInfo, + QuestTypePool questTypePool, + RepeatableQuestConfig repeatableConfig + ) + { + var questType = _randomUtil.DrawRandomFromList(questTypePool.Types).First(); + + // Get traders from whitelist and filter by quest type availability + var traders = repeatableConfig + .TraderWhitelist.Where(x => x.QuestTypes.Contains(questType)) + .Select(x => x.TraderId) + // filter out locked traders + .Where(x => pmcTraderInfo[x].Unlocked.GetValueOrDefault(false)) + .ToList(); + + var traderId = _randomUtil.DrawRandomFromList(traders).FirstOrDefault(); + + if (traderId is null) + { + // TODO: Localize me! + _logger.Error( + "Could not draw traderId from whitelist during repeatable quest generation" + ); + return null; + } + + if (_logger.IsLogEnabled(LogLevel.Debug)) + { + _logger.Debug($"Generating operation task type: {questType} for {traderId}"); + } + + return questType switch + { + "Elimination" => _eliminationQuestGenerator.Generate( + sessionId, + pmcLevel, + traderId, + questTypePool, + repeatableConfig + ), + "Completion" => _completionQuestGenerator.Generate( + sessionId, + pmcLevel, + traderId, + questTypePool, + repeatableConfig + ), + "Exploration" => _explorationQuestGenerator.Generate( + sessionId, + pmcLevel, + traderId, + questTypePool, + repeatableConfig + ), + "Pickup" => _pickupQuestGenerator.Generate( + sessionId, + pmcLevel, + traderId, + questTypePool, + repeatableConfig + ), + _ => null, + }; + } + /// /// Remove the provided quest from pmc and scav character profiles /// @@ -486,7 +570,7 @@ public class RepeatableQuestController( var currentTime = _timeUtil.GetTimeStamp(); // Daily / weekly / Daily_Savage - foreach (var repeatableConfig in _questConfig.RepeatableQuests) + foreach (var repeatableConfig in QuestConfig.RepeatableQuests) { // Get daily/weekly data from profile, add empty object if missing var generatedRepeatables = GetRepeatableQuestSubTypeFromProfile( @@ -544,7 +628,7 @@ public class RepeatableQuestController( var lifeline = 0; while (quest?.Id == null && questTypePool.Types.Count > 0) { - quest = _repeatableQuestGenerator.GenerateRepeatableQuest( + quest = PickAndGenerateRandomRepeatableQuest( sessionID, pmcData.Info.Level ?? 0, pmcData.TradersInfo, @@ -848,7 +932,7 @@ public class RepeatableQuestController( { return new QuestTypePool { - Types = _cloner.Clone(repeatableConfig.Types), + Types = _cloner.Clone(repeatableConfig.Types)!, Pool = new QuestPool { Exploration = new ExplorationPool diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs index 11262246..aae89922 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs @@ -3,6 +3,7 @@ using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; +using SPTarkov.Server.Core.Models.Spt.Repeatable; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; @@ -19,14 +20,17 @@ public class CompletionQuestGenerator( DatabaseService databaseService, SeasonalEventService seasonalEventService, LocalisationService localisationService, + ConfigServer configServer, RandomUtil randomUtil, MathUtil mathUtil, HashUtil hashUtil, ItemHelper itemHelper -) +) : IRepeatableQuestGenerator { protected const int MaxRandomNumberAttempts = 6; + protected QuestConfig QuestConfig = configServer.GetConfig(); + /// /// Generates a valid Completion quest /// @@ -42,6 +46,7 @@ public class CompletionQuestGenerator( string sessionId, int pmcLevel, string traderId, + QuestTypePool questTypePool, RepeatableQuestConfig repeatableConfig ) { diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/EliminationQuestGenerator.cs similarity index 56% rename from Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGenerator.cs rename to Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/EliminationQuestGenerator.cs index f013e7fa..bf05732c 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/EliminationQuestGenerator.cs @@ -1,7 +1,5 @@ using SPTarkov.DI.Annotations; -using SPTarkov.Server.Core.Generators.RepeatableQuestGeneration; using SPTarkov.Server.Core.Helpers; -using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; @@ -14,27 +12,24 @@ using SPTarkov.Server.Core.Utils.Cloners; using SPTarkov.Server.Core.Utils.Collections; using SPTarkov.Server.Core.Utils.Json; using BodyParts = SPTarkov.Server.Core.Constants.BodyParts; -using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; -namespace SPTarkov.Server.Core.Generators; +namespace SPTarkov.Server.Core.Generators.RepeatableQuestGeneration; -[Obsolete("In the process of being removed, do NOT add any new logic!!")] +// TODO: Refactor me! [Injectable] -public class RepeatableQuestGenerator( - ISptLogger _logger, - RandomUtil _randomUtil, - HashUtil _hashUtil, - MathUtil _mathUtil, - RepeatableQuestHelper _repeatableQuestHelper, - ItemHelper _itemHelper, - RepeatableQuestRewardGenerator _repeatableQuestRewardGenerator, - DatabaseService _databaseService, - LocalisationService _localisationService, - ConfigServer _configServer, - ICloner _cloner, - // This is temporary while this is being refactored, eventually these will all live in the RepeatableQuestController. - CompletionQuestGenerator _completionQuestGenerator -) +public class EliminationQuestGenerator( + ISptLogger logger, + RandomUtil randomUtil, + HashUtil hashUtil, + MathUtil mathUtil, + RepeatableQuestHelper repeatableQuestHelper, + ItemHelper itemHelper, + RepeatableQuestRewardGenerator repeatableQuestRewardGenerator, + DatabaseService databaseService, + LocalisationService localisationService, + ConfigServer configServer, + ICloner cloner +) : IRepeatableQuestGenerator { /// /// Body parts to present to the client as opposed to the body part information in quest data. @@ -47,87 +42,7 @@ public class RepeatableQuestGenerator( { BodyParts.Chest, [BodyParts.Chest, BodyParts.Stomach] }, }; - protected int _maxRandomNumberAttempts = 6; - protected QuestConfig _questConfig = _configServer.GetConfig(); - - /// - /// This method is called by /GetClientRepeatableQuests/ and creates one element of quest type format (see - /// assets/database/templates/repeatableQuests.json). - /// It randomly draws a quest type (currently Elimination, Completion or Exploration) as well as a trader who is - /// providing the quest - /// - /// Session id - /// Player's level for requested items and reward generation - /// Players trader standing/rep levels - /// Possible quest types pool - /// Repeatable quest config - /// RepeatableQuest - public RepeatableQuest? GenerateRepeatableQuest( - string sessionId, - int pmcLevel, - Dictionary pmcTraderInfo, - QuestTypePool questTypePool, - RepeatableQuestConfig repeatableConfig - ) - { - var questType = _randomUtil.DrawRandomFromList(questTypePool.Types).First(); - - // Get traders from whitelist and filter by quest type availability - var traders = repeatableConfig - .TraderWhitelist.Where(x => x.QuestTypes.Contains(questType)) - .Select(x => x.TraderId) - // filter out locked traders - .Where(x => pmcTraderInfo[x].Unlocked.GetValueOrDefault(false)) - .ToList(); - - var traderId = _randomUtil.DrawRandomFromList(traders).FirstOrDefault(); - - if (traderId is null) - { - // TODO: Localize me! - _logger.Error( - "Could not draw traderId from whitelist during repeatable quest generation" - ); - return null; - } - - if (_logger.IsLogEnabled(LogLevel.Debug)) - { - _logger.Debug($"Generating operation task type: {questType} for {traderId}"); - } - - return questType switch - { - "Elimination" => GenerateEliminationQuest( - sessionId, - pmcLevel, - traderId, - questTypePool, - repeatableConfig - ), - "Completion" => _completionQuestGenerator.Generate( - sessionId, - pmcLevel, - traderId, - repeatableConfig - ), - "Exploration" => GenerateExplorationQuest( - sessionId, - pmcLevel, - traderId, - questTypePool, - repeatableConfig - ), - "Pickup" => GeneratePickupQuest( - sessionId, - pmcLevel, - traderId, - questTypePool, - repeatableConfig - ), - _ => null, - }; - } + protected QuestConfig QuestConfig = configServer.GetConfig(); /// /// Generate a randomised Elimination quest @@ -138,10 +53,10 @@ public class RepeatableQuestGenerator( /// Pools for quests (used to avoid redundant quests) /// /// The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig - /// for the requestd quest + /// for the requested quest /// /// Object of quest type format for "Elimination" (see assets/database/templates/repeatableQuests.json) - protected RepeatableQuest GenerateEliminationQuest( + public RepeatableQuest? Generate( string sessionId, int pmcLevel, string traderId, @@ -151,29 +66,29 @@ public class RepeatableQuestGenerator( { var rand = new Random(); - var eliminationConfig = _repeatableQuestHelper.GetEliminationConfigByPmcLevel( + var eliminationConfig = repeatableQuestHelper.GetEliminationConfigByPmcLevel( pmcLevel, repeatableConfig ); var locationsConfig = repeatableConfig.Locations; var targetsConfig = new ProbabilityObjectArray( - _mathUtil, - _cloner, + mathUtil, + cloner, eliminationConfig.Targets ); var bodyPartsConfig = new ProbabilityObjectArray>( - _mathUtil, - _cloner, + mathUtil, + cloner, eliminationConfig.BodyParts ); var weaponCategoryRequirementConfig = new ProbabilityObjectArray>( - _mathUtil, - _cloner, + mathUtil, + cloner, eliminationConfig.WeaponCategoryRequirements ); var weaponRequirementConfig = new ProbabilityObjectArray>( - _mathUtil, - _cloner, + mathUtil, + cloner, eliminationConfig.WeaponRequirements ); @@ -233,7 +148,7 @@ public class RepeatableQuestGenerator( if ( locations.Contains("any") && ( - _randomUtil.GetChance100(eliminationConfig.SpecificLocationChance) + randomUtil.GetChance100(eliminationConfig.SpecificLocationChance) || locations.Count <= 1 ) ) @@ -248,7 +163,7 @@ public class RepeatableQuestGenerator( if (locations.Count > 0) { // Get name of location we want elimination to occur on - locationKey = _randomUtil.DrawRandomFromList(locations).FirstOrDefault(); + locationKey = randomUtil.DrawRandomFromList(locations).FirstOrDefault(); // Get a pool of locations the chosen bot type can be eliminated on if ( @@ -258,7 +173,7 @@ public class RepeatableQuestGenerator( ) ) { - _logger.Warning( + logger.Warning( $"Bot to kill: {botTypeToEliminate} not found in elimination dict" ); } @@ -279,8 +194,8 @@ public class RepeatableQuestGenerator( else { // Never should reach this if everything works out - _logger.Error( - _localisationService.GetText( + logger.Error( + localisationService.GetText( "quest-repeatable_elimination_generation_failed_please_report" ) ); @@ -290,13 +205,13 @@ public class RepeatableQuestGenerator( // draw the target body part and calculate the difficulty factor var bodyPartsToClient = new List(); var bodyPartDifficulty = 0d; - if (_randomUtil.GetChance100(eliminationConfig.BodyPartChance)) + if (randomUtil.GetChance100(eliminationConfig.BodyPartChance)) { // if we add a bodyPart condition, we draw randomly one or two parts // each bodyPart of the BODYPARTS ProbabilityObjectArray includes the string(s) which need to be presented to the client in ProbabilityObjectArray.data // e.g. we draw "Arms" from the probability array but must present ["LeftArm", "RightArm"] to the client bodyPartsToClient = []; - var bodyParts = bodyPartsConfig.Draw(_randomUtil.RandInt(1, 3), false); + var bodyParts = bodyPartsConfig.Draw(randomUtil.RandInt(1, 3), false); double probability = 0; foreach (var bodyPart in bodyParts) { @@ -326,7 +241,7 @@ public class RepeatableQuestGenerator( if (targetsConfig.Data(botTypeToEliminate)?.IsBoss ?? false) { // Get all boss spawn information - var bossSpawns = _databaseService + var bossSpawns = databaseService .GetLocations() .GetDictionary() .Select(x => x.Value) @@ -349,7 +264,7 @@ public class RepeatableQuestGenerator( } if ( - _randomUtil.GetChance100(eliminationConfig.DistanceProbability) + randomUtil.GetChance100(eliminationConfig.DistanceProbability) && isDistanceRequirementAllowed ) { @@ -368,7 +283,7 @@ public class RepeatableQuestGenerator( } string? allowedWeaponsCategory = null; - if (_randomUtil.GetChance100(eliminationConfig.WeaponCategoryRequirementProbability)) + if (randomUtil.GetChance100(eliminationConfig.WeaponCategoryRequirementProbability)) { // Filter out close range weapons from far distance requirement if (distance > 50) @@ -406,10 +321,10 @@ public class RepeatableQuestGenerator( { var weaponRequirement = weaponRequirementConfig.Draw(1, false); var specificAllowedWeaponCategory = weaponRequirementConfig.Data(weaponRequirement[0]); - var allowedWeapons = _itemHelper.GetItemTplsOfBaseType( + var allowedWeapons = itemHelper.GetItemTplsOfBaseType( specificAllowedWeaponCategory[0] ); - allowedWeapon = _randomUtil.GetArrayValue(allowedWeapons); + allowedWeapon = randomUtil.GetArrayValue(allowedWeapons); } // Draw how many npm kills are required @@ -434,9 +349,9 @@ public class RepeatableQuestGenerator( // Aforementioned issue makes it a bit crazy since now all easier quests give significantly lower rewards than Completion / Exploration // I therefore moved the mapping a bit up (from 0.2...1 to 0.5...2) so that normal difficulty still gives good reward and having the // crazy maximum difficulty will lead to a higher difficulty reward gain factor than 1 - var difficulty = _mathUtil.MapToRange(curDifficulty, minDifficulty, maxDifficulty, 0.5, 2); + var difficulty = mathUtil.MapToRange(curDifficulty, minDifficulty, maxDifficulty, 0.5, 2); - var quest = _repeatableQuestHelper.GenerateRepeatableTemplate( + var quest = repeatableQuestHelper.GenerateRepeatableTemplate( RepeatableQuestType.Elimination, traderId, repeatableConfig.Side, @@ -450,7 +365,7 @@ public class RepeatableQuestGenerator( } var availableForFinishCondition = quest.Conditions.AvailableForFinish[0]; - availableForFinishCondition.Counter.Id = _hashUtil.Generate(); + availableForFinishCondition.Counter.Id = hashUtil.Generate(); availableForFinishCondition.Counter.Conditions = []; // Only add specific location condition if specific map selected @@ -472,10 +387,12 @@ public class RepeatableQuestGenerator( ) ); availableForFinishCondition.Value = desiredKillCount; - availableForFinishCondition.Id = _hashUtil.Generate(); - quest.Location = GetQuestLocationByMapId(locationKey); + availableForFinishCondition.Id = hashUtil.Generate(); - quest.Rewards = _repeatableQuestRewardGenerator.GenerateReward( + // Get the quest location, default to any if none exist + quest.Location = repeatableQuestHelper.GetQuestLocationByMapId(locationKey) ?? "any"; + + quest.Rewards = repeatableQuestRewardGenerator.GenerateReward( pmcLevel, Math.Min(difficulty, 1), traderId, @@ -501,7 +418,7 @@ public class RepeatableQuestGenerator( { if (targetsConfig.Data(targetKey)?.IsBoss ?? false) { - return _randomUtil.RandInt( + return randomUtil.RandInt( eliminationConfig.MinBossKills, eliminationConfig.MaxBossKills + 1 ); @@ -509,13 +426,13 @@ public class RepeatableQuestGenerator( if (targetsConfig.Data(targetKey)?.IsPmc ?? false) { - return _randomUtil.RandInt( + return randomUtil.RandInt( eliminationConfig.MinPmcKills, eliminationConfig.MaxPmcKills + 1 ); } - return _randomUtil.RandInt(eliminationConfig.MinKills, eliminationConfig.MaxKills + 1); + return randomUtil.RandInt(eliminationConfig.MinKills, eliminationConfig.MaxKills + 1); } protected double DifficultyWeighing( @@ -540,7 +457,7 @@ public class RepeatableQuestGenerator( { return new QuestConditionCounterCondition { - Id = _hashUtil.Generate(), + Id = hashUtil.Generate(), DynamicLocale = true, Target = new ListOrT(location, null), ConditionType = "Location", @@ -566,7 +483,7 @@ public class RepeatableQuestGenerator( { var killConditionProps = new QuestConditionCounterCondition { - Id = _hashUtil.Generate(), + Id = hashUtil.Generate(), DynamicLocale = true, Target = new ListOrT(null, target), // e,g, "AnyPmc" Value = 1, @@ -613,239 +530,4 @@ public class RepeatableQuestGenerator( return killConditionProps; } - - /// - /// Generates a valid Exploration quest - /// - /// session id for the quest - /// player's level for reward generation - /// trader from which the quest will be provided - /// Pools for quests (used to avoid redundant quests) - /// - /// The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig - /// for the requested quest - /// - /// object of quest type format for "Exploration" (see assets/database/templates/repeatableQuests.json) - protected RepeatableQuest? GenerateExplorationQuest( - string sessionId, - int pmcLevel, - string traderId, - QuestTypePool questTypePool, - RepeatableQuestConfig repeatableConfig - ) - { - var explorationConfig = repeatableConfig.QuestConfig.Exploration; - var requiresSpecificExtract = - _randomUtil.Random.NextDouble() - < repeatableConfig.QuestConfig.Exploration.SpecificExits.Probability; - - if (questTypePool.Pool.Exploration.Locations.Count == 0) - { - // there are no more locations left for exploration; delete it as a possible quest type - questTypePool.Types = questTypePool.Types.Where(t => t != "Exploration").ToList(); - return null; - } - - // If location drawn is factory, it's possible to either get factory4_day and factory4_night or only one - // of the both - var locationKey = _randomUtil.DrawRandomFromDict(questTypePool.Pool.Exploration.Locations)[ - 0 - ]; - var locationTarget = questTypePool.Pool.Exploration.Locations[locationKey]; - - // Remove the location from the available pool - questTypePool.Pool.Exploration.Locations.Remove(locationKey); - - // Different max extract count when specific extract needed - var exitTimesMax = requiresSpecificExtract - ? explorationConfig.MaximumExtractsWithSpecificExit - : explorationConfig.MaximumExtracts + 1; - var numExtracts = _randomUtil.RandInt(1, exitTimesMax); - - var quest = _repeatableQuestHelper.GenerateRepeatableTemplate( - RepeatableQuestType.Exploration, - traderId, - repeatableConfig.Side, - sessionId - ); - - var exitStatusCondition = new QuestConditionCounterCondition - { - Id = _hashUtil.Generate(), - DynamicLocale = true, - Status = ["Survived"], - ConditionType = "ExitStatus", - }; - var locationCondition = new QuestConditionCounterCondition - { - Id = _hashUtil.Generate(), - DynamicLocale = true, - Target = new ListOrT(locationTarget, null), - ConditionType = "Location", - }; - - quest.Conditions.AvailableForFinish[0].Counter.Id = _hashUtil.Generate(); - quest.Conditions.AvailableForFinish[0].Counter.Conditions = - [ - exitStatusCondition, - locationCondition, - ]; - quest.Conditions.AvailableForFinish[0].Value = numExtracts; - quest.Conditions.AvailableForFinish[0].Id = _hashUtil.Generate(); - quest.Location = GetQuestLocationByMapId(locationKey.ToString()); - - if (requiresSpecificExtract) - { - // Fetch extracts for the requested side - var mapExits = GetLocationExitsForSide(locationKey.ToString(), repeatableConfig.Side); - - // Only get exits that have a greater than 0% chance to spawn - var exitPool = mapExits.Where(exit => exit.Chance > 0).ToList(); - - // Exclude exits with a requirement to leave (e.g. car extracts) - var possibleExits = exitPool - .Where(exit => - exit.PassageRequirement is not null - || repeatableConfig.QuestConfig.Exploration.SpecificExits.PassageRequirementWhitelist.Contains( - "PassageRequirement" - ) - ) - .ToList(); - - if (possibleExits.Count == 0) - { - _logger.Error( - $"Unable to choose specific exit on map: {locationKey}, Possible exit pool was empty" - ); - } - else - { - // Choose one of the exits we filtered above - var chosenExit = _randomUtil.DrawRandomFromList(possibleExits)[0]; - - // Create a quest condition to leave raid via chosen exit - var exitCondition = GenerateExplorationExitCondition(chosenExit); - quest.Conditions.AvailableForFinish[0].Counter.Conditions.Add(exitCondition); - } - } - - // Difficulty for exploration goes from 1 extract to maxExtracts - // Difficulty for reward goes from 0.2...1 -> map - var difficulty = _mathUtil.MapToRange( - numExtracts, - 1, - explorationConfig.MaximumExtracts, - 0.2, - 1 - ); - quest.Rewards = _repeatableQuestRewardGenerator.GenerateReward( - pmcLevel, - difficulty, - traderId, - repeatableConfig, - explorationConfig - ); - - return quest; - } - - /// - /// Filter a maps exits to just those for the desired side - /// - /// Map id (e.g. factory4_day) - /// Pmc/Scav - /// List of Exit objects - protected List GetLocationExitsForSide(string locationKey, PlayerGroup playerGroup) - { - var mapExtracts = _databaseService.GetLocation(locationKey.ToLower()).AllExtracts; - - return mapExtracts.Where(exit => exit.Side == Enum.GetName(playerGroup)).ToList(); - } - - protected RepeatableQuest GeneratePickupQuest( - string sessionId, - int pmcLevel, - string traderId, - QuestTypePool questTypePool, - RepeatableQuestConfig repeatableConfig - ) - { - var pickupConfig = repeatableConfig.QuestConfig.Pickup; - - var quest = _repeatableQuestHelper.GenerateRepeatableTemplate( - RepeatableQuestType.Pickup, - traderId, - repeatableConfig.Side, - sessionId - ); - - var itemTypeToFetchWithCount = _randomUtil.GetArrayValue( - pickupConfig.ItemTypeToFetchWithMaxCount - ); - var itemCountToFetch = _randomUtil.RandInt( - itemTypeToFetchWithCount.MinimumPickupCount.Value, - itemTypeToFetchWithCount.MaximumPickupCount + 1 - ); - // Choose location - doesnt seem to work for anything other than 'any' - // var locationKey: string = this.randomUtil.drawRandomFromDict(questTypePool.pool.Pickup.locations)[0]; - // var locationTarget = questTypePool.pool.Pickup.locations[locationKey]; - - var findCondition = quest.Conditions.AvailableForFinish.FirstOrDefault(x => - x.ConditionType == "FindItem" - ); - findCondition.Target = new ListOrT([itemTypeToFetchWithCount.ItemType], null); - findCondition.Value = itemCountToFetch; - - var counterCreatorCondition = quest.Conditions.AvailableForFinish.FirstOrDefault(x => - x.ConditionType == "CounterCreator" - ); - // var locationCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Location"); - // (locationCondition._props as ILocationConditionProps).target = [...locationTarget]; - - var equipmentCondition = counterCreatorCondition.Counter.Conditions.FirstOrDefault(x => - x.ConditionType == "Equipment" - ); - equipmentCondition.EquipmentInclusive = - [ - [itemTypeToFetchWithCount.ItemType], - ]; - - // Add rewards - quest.Rewards = _repeatableQuestRewardGenerator.GenerateReward( - pmcLevel, - 1, - traderId, - repeatableConfig, - pickupConfig - ); - - return quest; - } - - /// - /// Convert a location into an quest code can read (e.g. factory4_day into 55f2d3fd4bdc2d5f408b4567) - /// - /// e.g factory4_day - /// guid - protected string GetQuestLocationByMapId(string locationKey) - { - return _questConfig.LocationIdMap[locationKey]; - } - - /// - /// Exploration repeatable quests can specify a required extraction point. - /// This method creates the according object which will be appended to the conditions list - /// - /// The exit name to generate the condition for - /// Exit condition - protected QuestConditionCounterCondition GenerateExplorationExitCondition(Exit exit) - { - return new QuestConditionCounterCondition - { - Id = _hashUtil.Generate(), - DynamicLocale = true, - ExitName = exit.Name, - ConditionType = "ExitName", - }; - } } diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs new file mode 100644 index 00000000..6216fe9a --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs @@ -0,0 +1,319 @@ +using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Helpers; +using SPTarkov.Server.Core.Models.Eft.Common; +using SPTarkov.Server.Core.Models.Eft.Common.Tables; +using SPTarkov.Server.Core.Models.Enums; +using SPTarkov.Server.Core.Models.Spt.Config; +using SPTarkov.Server.Core.Models.Spt.Repeatable; +using SPTarkov.Server.Core.Models.Utils; +using SPTarkov.Server.Core.Servers; +using SPTarkov.Server.Core.Services; +using SPTarkov.Server.Core.Utils; +using SPTarkov.Server.Core.Utils.Json; + +namespace SPTarkov.Server.Core.Generators.RepeatableQuestGeneration; + +[Injectable] +public class ExplorationQuestGenerator( + ISptLogger logger, + RepeatableQuestHelper repeatableQuestHelper, + RepeatableQuestRewardGenerator repeatableQuestRewardGenerator, + DatabaseService databaseService, + LocalisationService localisationService, + ConfigServer configServer, + RandomUtil randomUtil, + MathUtil mathUtil, + HashUtil hashUtil + ) : IRepeatableQuestGenerator +{ + protected record LocationInfo( + ELocationName LocationName, + List LocationTarget, + bool RequiresSpecificExtract, + int NumOfExtractsRequired + ); + + protected QuestConfig QuestConfig = configServer.GetConfig(); + + /// + /// Generates a valid Exploration quest + /// + /// session id for the quest + /// player's level for reward generation + /// trader from which the quest will be provided + /// Pools for quests (used to avoid redundant quests) + /// + /// The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig + /// for the requested quest + /// + /// object of quest type format for "Exploration" (see assets/database/templates/repeatableQuests.json) + public RepeatableQuest? Generate( + string sessionId, + int pmcLevel, + string traderId, + QuestTypePool questTypePool, + RepeatableQuestConfig repeatableConfig + ) + { + var explorationConfig = repeatableConfig.QuestConfig.Exploration; + + // Try and get a location to generate for + if (!TryGetLocationInfo(repeatableConfig, explorationConfig, questTypePool, out var locationInfo) + || locationInfo is null) + { + // TODO - Localize me + logger.Warning("Generating exploration repeatable quest failed, no remaining locations available"); + return null; + } + + // Generate the quest template + var quest = repeatableQuestHelper.GenerateRepeatableTemplate( + RepeatableQuestType.Exploration, + traderId, + repeatableConfig.Side, + sessionId + ); + + if (quest is null) + { + // TODO - Localize me + logger.Error("Generating quest failed, no quest template available"); + return null; + } + + // Generate the available for finish exit condition + if (!TryGenerateAvailableForFinish(quest, locationInfo)) + { + // TODO - Localize me + logger.Error($"Generating AvailableForFinish failed for location {locationInfo.LocationName}"); + return null; + } + + // If we require a specific extract requirement, generate it + if (locationInfo.RequiresSpecificExtract + && !TryGenerateSpecificExtractRequirement(quest, repeatableConfig, locationInfo)) + { + // TODO - Localize me + logger.Error($"Generating SpecificExtractRequirement failed for location {locationInfo.LocationName}"); + return null; + } + + // Difficulty for exploration goes from 1 extract to maxExtracts + // Difficulty for reward goes from 0.2...1 -> map + var difficulty = mathUtil.MapToRange( + locationInfo.NumOfExtractsRequired, + 1, + explorationConfig.MaximumExtracts, + 0.2, + 1 + ); + quest.Rewards = repeatableQuestRewardGenerator.GenerateReward( + pmcLevel, + difficulty, + traderId, + repeatableConfig, + explorationConfig + ); + + return quest; + } + + /// + /// Draws a location from the exploration location pool + /// + /// + /// + /// Pool to draw from + /// Location chosen + /// True if location selected, false if no locations remain + protected bool TryGetLocationInfo( + RepeatableQuestConfig repeatableConfig, + Exploration explorationConfig, + QuestTypePool pool, + out LocationInfo? locationInfo) + { + if (pool.Pool?.Exploration?.Locations?.Count is null or 0) + { + // there are no more locations left for exploration; delete it as a possible quest type + pool.Types = pool.Types?.Where(t => t != "Exploration").ToList(); + locationInfo = null; + return false; + } + + // If location drawn is factory, it's possible to either get factory4_day and factory4_night use index 0, + // as the key is factory4_day + var locationKey = randomUtil.DrawRandomFromDict(pool.Pool.Exploration.Locations)[ + 0 + ]; + + // Make the location info object + var locationTarget = pool.Pool!.Exploration!.Locations![locationKey]; + + var requiresSpecificExtract = + randomUtil.GetChance100(repeatableConfig.QuestConfig.Exploration.SpecificExits.Chance); + + var numExtracts = GetNumberOfExits(explorationConfig, requiresSpecificExtract); + + locationInfo = new LocationInfo(locationKey, locationTarget.ToList(), requiresSpecificExtract, numExtracts); + + // Remove the location from the available pool + pool.Pool.Exploration.Locations.Remove(locationKey); + + return true; + } + + /// + /// Get the number of times the player needs to exit + /// + /// Exploration config + /// Is this a specific extract + /// Number of exit requirements + protected int GetNumberOfExits(Exploration config, bool requiresSpecificExtract) + { + // Different max extract count when specific extract needed + var exitTimesMax = requiresSpecificExtract + ? config.MaximumExtractsWithSpecificExit + : config.MaximumExtracts + 1; + + return randomUtil.RandInt(1, exitTimesMax); + } + + /// + /// Filter a maps exits to just those for the desired side + /// + /// Map id (e.g. factory4_day) + /// Pmc/Scav + /// List of Exit objects + protected List? GetLocationExitsForSide(string locationKey, PlayerGroup playerGroup) + { + var mapExtracts = databaseService.GetLocation(locationKey.ToLower())?.AllExtracts; + + return mapExtracts?.Where(exit => exit.Side == Enum.GetName(playerGroup)).ToList(); + } + + /// + /// Generate the initial available for finish condition + /// + /// quest to add the condition to + /// LocationInfo object with the generated data + /// True if generated, false if not + protected bool TryGenerateAvailableForFinish(RepeatableQuest quest, LocationInfo locationInfo) + { + // This should never be hit, this is here to shut the compiler up. + if (quest.Conditions.AvailableForFinish?[0].Counter is null) + { + logger.Error("Counter is null, something has gone terribly wrong"); + return false; + } + + // Lookup the location + var location = repeatableQuestHelper.GetQuestLocationByMapId(locationInfo.LocationName.ToString()); + + if (location is null) + { + // TODO - Localize me + logger.Error($"Unable to get locationId for {locationInfo.LocationName}"); + return false; + } + + var exitStatusCondition = new QuestConditionCounterCondition + { + Id = hashUtil.Generate(), + DynamicLocale = true, + Status = ["Survived"], + ConditionType = "ExitStatus", + }; + + var locationCondition = new QuestConditionCounterCondition + { + Id = hashUtil.Generate(), + DynamicLocale = true, + Target = new ListOrT(locationInfo.LocationTarget, null), + ConditionType = "Location", + }; + + quest.Conditions.AvailableForFinish![0].Counter!.Id = hashUtil.Generate(); + quest.Conditions.AvailableForFinish![0].Counter!.Conditions = + [ + exitStatusCondition, + locationCondition, + ]; + quest.Conditions.AvailableForFinish[0].Value = locationInfo.NumOfExtractsRequired; + quest.Conditions.AvailableForFinish[0].Id = hashUtil.Generate(); + + + + quest.Location = location; + + return true; + } + + /// + /// Adds a specific extract requirement to the quest + /// + /// quest to add it to + /// repeatable config + /// LocationInfo object with the generated data + /// True if generated, false if not + protected bool TryGenerateSpecificExtractRequirement(RepeatableQuest quest, RepeatableQuestConfig repeatableConfig, LocationInfo locationInfo) + { + // Fetch extracts for the requested side + var mapExits = GetLocationExitsForSide(locationInfo.LocationName.ToString(), repeatableConfig.Side); + + if (mapExits is null) + { + // TODO: Localize me + logger.Error($"Unable to get location list for location {locationInfo.LocationName}"); + return false; + } + + // Only get exits that have a greater than 0% chance to spawn + var exitPool = mapExits.Where(exit => exit.Chance > 0).ToList(); + + // Exclude exits with a requirement to leave (e.g. car extracts) + var possibleExits = exitPool + .Where(exit => + exit.PassageRequirement is not null + || repeatableConfig.QuestConfig.Exploration.SpecificExits.PassageRequirementWhitelist.Contains( + "PassageRequirement" + ) + ) + .ToList(); + + if (possibleExits.Count == 0) + { + // TODO - Localize me! + logger.Error( + $"Unable to choose specific exit on map: {locationInfo.LocationName}, Possible exit pool was empty" + ); + + return false; + } + + // Choose one of the exits we filtered above + var chosenExit = randomUtil.DrawRandomFromList(possibleExits)[0]; + + // Create a quest condition to leave raid via chosen exit + var exitCondition = GenerateQuestConditionCounter(chosenExit); + quest.Conditions.AvailableForFinish![0].Counter!.Conditions!.Add(exitCondition); + + return true; + } + + /// + /// Exploration repeatable quests can specify a required extraction point. + /// This method creates the according object which will be appended to the conditions list + /// + /// The exit name to generate the condition for + /// Exit condition + protected QuestConditionCounterCondition GenerateQuestConditionCounter(Exit exit) + { + return new QuestConditionCounterCondition + { + Id = hashUtil.Generate(), + DynamicLocale = true, + ExitName = exit.Name, + ConditionType = "ExitName", + }; + } +} diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/IRepeatableQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/IRepeatableQuestGenerator.cs new file mode 100644 index 00000000..6a37f187 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/IRepeatableQuestGenerator.cs @@ -0,0 +1,16 @@ +using SPTarkov.Server.Core.Models.Eft.Common.Tables; +using SPTarkov.Server.Core.Models.Spt.Config; +using SPTarkov.Server.Core.Models.Spt.Repeatable; + +namespace SPTarkov.Server.Core.Generators.RepeatableQuestGeneration; + +public interface IRepeatableQuestGenerator +{ + public RepeatableQuest? Generate( + string sessionId, + int pmcLevel, + string traderId, + QuestTypePool questTypePool, + RepeatableQuestConfig repeatableConfig + ); +} diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/PickupQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/PickupQuestGenerator.cs new file mode 100644 index 00000000..5b288f06 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/PickupQuestGenerator.cs @@ -0,0 +1,87 @@ +using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Helpers; +using SPTarkov.Server.Core.Models.Eft.Common.Tables; +using SPTarkov.Server.Core.Models.Enums; +using SPTarkov.Server.Core.Models.Spt.Config; +using SPTarkov.Server.Core.Models.Spt.Repeatable; +using SPTarkov.Server.Core.Models.Utils; +using SPTarkov.Server.Core.Services; +using SPTarkov.Server.Core.Utils; +using SPTarkov.Server.Core.Utils.Json; + +namespace SPTarkov.Server.Core.Generators.RepeatableQuestGeneration; + +[Injectable] +public class PickupQuestGenerator( + ISptLogger logger, + RepeatableQuestHelper repeatableQuestHelper, + RepeatableQuestRewardGenerator repeatableQuestRewardGenerator, + DatabaseService databaseService, + LocalisationService localisationService, + RandomUtil randomUtil, + MathUtil mathUtil, + HashUtil hashUtil +) : IRepeatableQuestGenerator +{ + + // TODO: This isn't really implemented well at all, what even is this. + public RepeatableQuest? Generate( + string sessionId, + int pmcLevel, + string traderId, + QuestTypePool questTypePool, + RepeatableQuestConfig repeatableConfig) + { + var pickupConfig = repeatableConfig.QuestConfig.Pickup; + + var quest = repeatableQuestHelper.GenerateRepeatableTemplate( + RepeatableQuestType.Pickup, + traderId, + repeatableConfig.Side, + sessionId + ); + + var itemTypeToFetchWithCount = randomUtil.GetArrayValue( + pickupConfig.ItemTypeToFetchWithMaxCount + ); + + var itemCountToFetch = randomUtil.RandInt( + itemTypeToFetchWithCount.MinimumPickupCount.Value, + itemTypeToFetchWithCount.MaximumPickupCount + 1 + ); + // Choose location - doesn't seem to work for anything other than 'any' + // var locationKey: string = this.randomUtil.drawRandomFromDict(questTypePool.pool.Pickup.locations)[0]; + // var locationTarget = questTypePool.pool.Pickup.locations[locationKey]; + + var findCondition = quest.Conditions.AvailableForFinish.FirstOrDefault(x => + x.ConditionType == "FindItem" + ); + findCondition.Target = new ListOrT([itemTypeToFetchWithCount.ItemType], null); + findCondition.Value = itemCountToFetch; + + var counterCreatorCondition = quest.Conditions.AvailableForFinish.FirstOrDefault(x => + x.ConditionType == "CounterCreator" + ); + // var locationCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Location"); + // (locationCondition._props as ILocationConditionProps).target = [...locationTarget]; + + var equipmentCondition = counterCreatorCondition.Counter.Conditions.FirstOrDefault(x => + x.ConditionType == "Equipment" + ); + equipmentCondition.EquipmentInclusive = + [ + [itemTypeToFetchWithCount.ItemType], + ]; + + // Add rewards + quest.Rewards = repeatableQuestRewardGenerator.GenerateReward( + pmcLevel, + 1, + traderId, + repeatableConfig, + pickupConfig + ); + + return quest; + } +} diff --git a/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs index 903b78e4..02db4fe9 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs @@ -19,7 +19,7 @@ public class RepeatableQuestHelper( ConfigServer configServer ) { - protected QuestConfig _questConfig = configServer.GetConfig(); + protected QuestConfig QuestConfig = configServer.GetConfig(); /// /// Get the relevant elimination config based on the current players PMC level @@ -45,7 +45,7 @@ public class RepeatableQuestHelper( /// public Dictionary GetRepeatableQuestTemplatesByGroup(PlayerGroup playerGroup) { - var templates = _questConfig.RepeatableQuestTemplates; + var templates = QuestConfig.RepeatableQuestTemplates; return playerGroup switch { @@ -196,4 +196,21 @@ public class RepeatableQuestHelper( return questData; } + + /// + /// Convert a raw location string into a location code can read (e.g. factory4_day into 55f2d3fd4bdc2d5f408b4567) + /// + /// e.g. factory4_day + /// guid + public string? GetQuestLocationByMapId(string locationKey) + { + if (!QuestConfig.LocationIdMap.TryGetValue(locationKey, out var locationId)) + { + // TODO - localize me! + logger.Error($"No location in LocationIdMap found for key {locationKey}"); + return null; + } + + return locationId; + } } diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs index 4f0f8615..d5db38d9 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs @@ -406,8 +406,8 @@ public record SpecificExits /// /// Chance that an operational task is generated with a specific extract /// - [JsonPropertyName("probability")] - public required double Probability { get; set; } + [JsonPropertyName("chance")] + public required double Chance { get; set; } /// /// Whitelist of specific extract types diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Repeatable/QuestTypePool.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Repeatable/QuestTypePool.cs index 92b7df0a..3300e357 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Repeatable/QuestTypePool.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Repeatable/QuestTypePool.cs @@ -9,10 +9,10 @@ public record QuestTypePool public Dictionary ExtensionData { get; set; } [JsonPropertyName("types")] - public List? Types { get; set; } + public required List Types { get; set; } [JsonPropertyName("pool")] - public QuestPool? Pool { get; set; } + public required QuestPool Pool { get; set; } } public record QuestPool @@ -21,13 +21,13 @@ public record QuestPool public Dictionary ExtensionData { get; set; } [JsonPropertyName("Exploration")] - public ExplorationPool? Exploration { get; set; } + public required ExplorationPool Exploration { get; set; } [JsonPropertyName("Elimination")] - public EliminationPool? Elimination { get; set; } + public required EliminationPool Elimination { get; set; } [JsonPropertyName("Pickup")] - public ExplorationPool? Pickup { get; set; } + public required ExplorationPool Pickup { get; set; } } public record ExplorationPool