From 371c9d58f0982dda76bb23ddcb3457db4959d126 Mon Sep 17 00:00:00 2001 From: Cj <161484149+CJ-SPT@users.noreply.github.com> Date: Wed, 2 Jul 2025 07:44:49 -0400 Subject: [PATCH] Repeatable quest generation MongoID conversion (#439) * Update repeatable quest generation for mongoid, add new server localizations, switch `Traders` to MongoId * Give error default value --- .../SPT_Data/database/locales/server/en.json | 4 + .../Controllers/RepeatableQuestController.cs | 158 +++++++++--------- .../Controllers/TraderController.cs | 39 ++--- .../Extensions/MathExtensions.cs | 24 +++ .../CompletionQuestGenerator.cs | 6 +- .../EliminationQuestGenerator.cs | 17 +- .../ExplorationQuestGenerator.cs | 3 +- .../IRepeatableQuestGenerator.cs | 5 +- .../PickupQuestGenerator.cs | 3 +- .../RepeatableQuestRewardGenerator.cs | 17 +- .../Helpers/RepeatableQuestHelper.cs | 35 ++-- .../Models/Common/MongoId.cs | 17 +- .../Models/Enums/Traders.cs | 24 +-- .../Models/Spt/Config/QuestConfig.cs | 24 +-- .../Services/LocationLifecycleService.cs | 9 +- 15 files changed, 222 insertions(+), 163 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json index 5cdd11ff..33f66292 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json @@ -677,6 +677,10 @@ "repeatable-elimination-config-not-found": "Unable to find the elimination config", "repeatable-elimination-any-not-found": "We're not targeting a specific location and `any` was not found in locations", "repeatable-elimination-specific-weapon-null": "Specific allowed weapon categories are null", + "repeatable-quest_helper_template_not_found": "No repeatable quest template found for type: %s", + "repeatable-quest_helper_template_name_not_found": "Could not resolve template name for: %s", + "repeatable-quest_helper_no_status": "No quest status found for type: %s", + "repeatable-quest_helper_no_loc_id": "No location in LocationIdMap found for key: %s", "reward-type_not_handled": "Reward type: {{rewardType}} not handled for quest/achievement: {{questId}}", "reward-unable_to_find_matching_hideout_production": "Unable to find matching hideout craft unlock for quest/achievement: {{questId}}, matches found: {{matchCount}}", "route_onupdate_no_response": "onUpdate: %s route doesn't report success or fail", diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs index ad95fff8..344feb71 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs @@ -2,6 +2,7 @@ using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Generators.RepeatableQuestGeneration; using SPTarkov.Server.Core.Helpers; +using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Eft.ItemEvent; @@ -24,29 +25,28 @@ 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, - HttpResponseUtil _httpResponseUtil, - ProfileHelper _profileHelper, - ProfileFixerService _profileFixerService, - ServerLocalisationService _serverLocalisationService, - EventOutputHolder _eventOutputHolder, - PaymentService _paymentService, - RepeatableQuestHelper _repeatableQuestHelper, - QuestHelper _questHelper, - DatabaseService _databaseService, - ConfigServer _configServer, - ICloner _cloner + ISptLogger logger, + EliminationQuestGenerator eliminationQuestGenerator, + CompletionQuestGenerator completionQuestGenerator, + ExplorationQuestGenerator explorationQuestGenerator, + PickupQuestGenerator pickupQuestGenerator, + TimeUtil timeUtil, + RandomUtil randomUtil, + HttpResponseUtil httpResponseUtil, + ProfileHelper profileHelper, + ProfileFixerService profileFixerService, + ServerLocalisationService serverLocalisationService, + EventOutputHolder eventOutputHolder, + PaymentService paymentService, + RepeatableQuestHelper repeatableQuestHelper, + QuestHelper questHelper, + DatabaseService databaseService, + ConfigServer configServer, + ICloner cloner ) { protected static readonly List _questTypes = ["PickUp", "Exploration", "Elimination"]; - protected readonly QuestConfig QuestConfig = _configServer.GetConfig(); + protected readonly QuestConfig QuestConfig = configServer.GetConfig(); /// /// Handle the client accepting a repeatable quest and starting it @@ -64,7 +64,7 @@ public class RepeatableQuestController( ) { // Create and store quest status object inside player profile - var newRepeatableQuest = _questHelper.GetQuestReadyForProfile( + var newRepeatableQuest = questHelper.GetQuestReadyForProfile( pmcData, QuestStatusEnum.Started, acceptedQuest @@ -75,15 +75,15 @@ public class RepeatableQuestController( var repeatableQuestProfile = GetRepeatableQuestFromProfile(pmcData, acceptedQuest.QuestId); if (repeatableQuestProfile is null) { - _logger.Error( - _serverLocalisationService.GetText( + logger.Error( + serverLocalisationService.GetText( "repeatable-accepted_repeatable_quest_not_found_in_active_quests", acceptedQuest.QuestId ) ); throw new Exception( - _serverLocalisationService.GetText("repeatable-unable_to_accept_quest_see_log") + serverLocalisationService.GetText("repeatable-unable_to_accept_quest_see_log") ); } @@ -93,13 +93,13 @@ public class RepeatableQuestController( && _questTypes.Contains(repeatableQuestProfile.Type.ToString()) ) { - var fullProfile = _profileHelper.GetFullProfile(sessionID); + var fullProfile = profileHelper.GetFullProfile(sessionID); fullProfile.CharacterData.ScavData.Quests ??= []; fullProfile.CharacterData.ScavData.Quests.Add(newRepeatableQuest); } - var response = _eventOutputHolder.GetOutput(sessionID); + var response = eventOutputHolder.GetOutput(sessionID); return response; } @@ -117,9 +117,9 @@ public class RepeatableQuestController( string sessionID ) { - var output = _eventOutputHolder.GetOutput(sessionID); + var output = eventOutputHolder.GetOutput(sessionID); - var fullProfile = _profileHelper.GetFullProfile(sessionID); + var fullProfile = profileHelper.GetFullProfile(sessionID); // Check for existing quest in (daily/weekly/scav arrays) var repeatables = GetRepeatableById(changeRequest.QuestId, pmcData); @@ -128,12 +128,12 @@ public class RepeatableQuestController( if (repeatables.RepeatableType is null || repeatables.Quest is null) { // Unable to find quest being replaced - var message = _serverLocalisationService.GetText( + var message = serverLocalisationService.GetText( "quest-unable_to_find_repeatable_to_replace" ); - _logger.Error(message); + logger.Error(message); - return _httpResponseUtil.AppendErrorToOutput(output, message); + return httpResponseUtil.AppendErrorToOutput(output, message); } // Subtype name of quest - daily/weekly/scav @@ -148,7 +148,7 @@ public class RepeatableQuestController( .ToList(); // Save for later cost calculations - var previousChangeRequirement = _cloner.Clone( + var previousChangeRequirement = cloner.Clone( repeatablesOfTypeInProfile.ChangeRequirement[changeRequest.QuestId] ); @@ -182,18 +182,18 @@ public class RepeatableQuestController( // Unable to find quest being replaced var message = $"Unable to generate repeatable quest of type: {repeatableTypeLower} to replace trader: {replacedQuestTraderId} quest: {changeRequest.QuestId}"; - _logger.Error(message); + logger.Error(message); - return _httpResponseUtil.AppendErrorToOutput(output, message); + return httpResponseUtil.AppendErrorToOutput(output, message); } // Add newly generated quest to daily/weekly/scav type array newRepeatableQuest.Side = Enum.GetName(repeatableConfig.Side); repeatablesOfTypeInProfile.ActiveQuests.Add(newRepeatableQuest); - if (_logger.IsLogEnabled(LogLevel.Debug)) + if (logger.IsLogEnabled(LogLevel.Debug)) { - _logger.Debug( + logger.Debug( $"Removing: {repeatableConfig.Name} quest: {questToReplace.Id} from trader: {questToReplace.TraderId} as its been replaced" ); } @@ -208,7 +208,7 @@ public class RepeatableQuestController( repeatablesOfTypeInProfile.ChangeRequirement[newRepeatableQuest.Id] = new ChangeRequirement { ChangeCost = newRepeatableQuest.ChangeCost, - ChangeStandingCost = _randomUtil.GetArrayValue(repeatableConfig.StandingChangeCost), + ChangeStandingCost = randomUtil.GetArrayValue(repeatableConfig.StandingChangeCost), }; // Check if we should charge player for replacing quest @@ -231,7 +231,7 @@ public class RepeatableQuestController( Math.Truncate( cost.Count.Value * (1 - (Math.Truncate(charismaBonus / 100) * 0.001)) ); - _paymentService.AddPaymentToOutput( + paymentService.AddPaymentToOutput( pmcData, cost.TemplateId, cost.Count.Value, @@ -246,7 +246,7 @@ public class RepeatableQuestController( } // Clone data before we send it to client - var repeatableToChangeClone = _cloner.Clone(repeatablesOfTypeInProfile); + var repeatableToChangeClone = cloner.Clone(repeatablesOfTypeInProfile); // Purge inactive repeatables repeatableToChangeClone.InactiveQuests = []; @@ -275,9 +275,9 @@ public class RepeatableQuestController( continue; } - if (_logger.IsLogEnabled(LogLevel.Debug)) + if (logger.IsLogEnabled(LogLevel.Debug)) { - _logger.Debug($"Accepted repeatable quest: {questId} from: {repeatableQuest.Name}"); + logger.Debug($"Accepted repeatable quest: {questId} from: {repeatableQuest.Name}"); } matchingQuest.SptRepatableGroupName = repeatableQuest.Name; @@ -312,7 +312,7 @@ public class RepeatableQuestController( } // Only certain game versions have access to free refreshes - var hasAccessToFreeRefreshSystem = _profileHelper.HasAccessToRepeatableFreeRefreshSystem( + var hasAccessToFreeRefreshSystem = profileHelper.HasAccessToRepeatableFreeRefreshSystem( fullProfile.CharacterData.PmcData ); @@ -396,8 +396,8 @@ public class RepeatableQuestController( if (attempts > maxAttempts) { - _logger.Error( - _serverLocalisationService.GetText( + logger.Error( + serverLocalisationService.GetText( "quest-repeatable_generation_failed_please_report", attempts ) @@ -427,55 +427,55 @@ public class RepeatableQuestController( RepeatableQuestConfig repeatableConfig ) { - var questType = _randomUtil.DrawRandomFromList(questTypePool.Types).First(); + 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)) + .TraderWhitelist.Where(whitelist => whitelist.QuestTypes.Contains(questType)) .Select(x => x.TraderId) // filter out locked traders - .Where(x => pmcTraderInfo[x].Unlocked.GetValueOrDefault(false)) + .Where(mongoId => pmcTraderInfo[mongoId].Unlocked.GetValueOrDefault(false)) .ToList(); - var traderId = _randomUtil.DrawRandomFromList(traders).FirstOrDefault(); - if (traderId is null) + var traderId = randomUtil.DrawRandomFromList(traders).FirstOrDefault(); + if (traderId.IsEmpty()) { - _logger.Error( - _serverLocalisationService.GetText("repeatable-unable_to_find_trader_in_pool") + logger.Error( + serverLocalisationService.GetText("repeatable-unable_to_find_trader_in_pool") ); return null; } - if (_logger.IsLogEnabled(LogLevel.Debug)) + if (logger.IsLogEnabled(LogLevel.Debug)) { - _logger.Debug($"Generating operation task type: {questType} for {traderId}"); + logger.Debug($"Generating operation task type: {questType} for {traderId}"); } return questType switch { - "Elimination" => _eliminationQuestGenerator.Generate( + "Elimination" => eliminationQuestGenerator.Generate( sessionId, pmcLevel, traderId, questTypePool, repeatableConfig ), - "Completion" => _completionQuestGenerator.Generate( + "Completion" => completionQuestGenerator.Generate( sessionId, pmcLevel, traderId, questTypePool, repeatableConfig ), - "Exploration" => _explorationQuestGenerator.Generate( + "Exploration" => explorationQuestGenerator.Generate( sessionId, pmcLevel, traderId, questTypePool, repeatableConfig ), - "Pickup" => _pickupQuestGenerator.Generate( + "Pickup" => pickupQuestGenerator.Generate( sessionId, pmcLevel, traderId, @@ -494,7 +494,7 @@ public class RepeatableQuestController( protected void RemoveQuestFromProfile(SptProfile fullProfile, string questToReplaceId) { // Find quest we're replacing in pmc profile quests array and remove it - _questHelper.FindAndRemoveQuestFromArrayIfExists( + questHelper.FindAndRemoveQuestFromArrayIfExists( questToReplaceId, fullProfile.CharacterData.PmcData.Quests ); @@ -502,7 +502,7 @@ public class RepeatableQuestController( // Look for and remove quest we're replacing in scav profile too if (fullProfile.CharacterData.ScavData is not null) { - _questHelper.FindAndRemoveQuestFromArrayIfExists( + questHelper.FindAndRemoveQuestFromArrayIfExists( questToReplaceId, fullProfile.CharacterData.ScavData.Quests ); @@ -563,9 +563,9 @@ public class RepeatableQuestController( public List GetClientRepeatableQuests(string sessionID) { var returnData = new List(); - var fullProfile = _profileHelper.GetFullProfile(sessionID); + var fullProfile = profileHelper.GetFullProfile(sessionID); var pmcData = fullProfile.CharacterData.PmcData; - var currentTime = _timeUtil.GetTimeStamp(); + var currentTime = timeUtil.GetTimeStamp(); // Daily / weekly / Daily_Savage foreach (var repeatableConfig in QuestConfig.RepeatableQuests) @@ -589,9 +589,9 @@ public class RepeatableQuestController( { returnData.Add(generatedRepeatables); - if (_logger.IsLogEnabled(LogLevel.Debug)) + if (logger.IsLogEnabled(LogLevel.Debug)) { - _logger.Debug($"[Quest Check] {repeatableTypeLower} quests are still valid."); + logger.Debug($"[Quest Check] {repeatableTypeLower} quests are still valid."); } continue; @@ -602,9 +602,9 @@ public class RepeatableQuestController( // Set endtime to be now + new duration generatedRepeatables.EndTime = currentTime + repeatableConfig.ResetTime; generatedRepeatables.InactiveQuests = []; - if (_logger.IsLogEnabled(LogLevel.Debug)) + if (logger.IsLogEnabled(LogLevel.Debug)) { - _logger.Debug($"Generating new {repeatableTypeLower}"); + logger.Debug($"Generating new {repeatableTypeLower}"); } // Put old quests to inactive (this is required since only then the client makes them fail due to non-completion) @@ -636,7 +636,7 @@ public class RepeatableQuestController( lifeline++; if (lifeline > 10) { - _logger.Error( + logger.Error( "We were stuck in repeatable quest generation. This should never happen. Please report" ); @@ -669,7 +669,7 @@ public class RepeatableQuestController( new ChangeRequirement { ChangeCost = quest.ChangeCost, - ChangeStandingCost = _randomUtil.GetArrayValue( + ChangeStandingCost = randomUtil.GetArrayValue( repeatableConfig.StandingChangeCost ), // Randomise standing loss to replace } @@ -712,7 +712,7 @@ public class RepeatableQuestController( var repeatableQuestDetails = pmcData.RepeatableQuests.FirstOrDefault(repeatable => repeatable.Name == repeatableConfig.Name ); - var hasAccess = _profileHelper.HasAccessToRepeatableFreeRefreshSystem(pmcData); + var hasAccess = profileHelper.HasAccessToRepeatableFreeRefreshSystem(pmcData); if (repeatableQuestDetails is null) { @@ -766,9 +766,9 @@ public class RepeatableQuestController( // Scav and daily quests not unlocked yet if (repeatableConfig.Side == PlayerGroup.Scav && !PlayerHasDailyScavQuestsUnlocked(pmcData)) { - if (_logger.IsLogEnabled(LogLevel.Debug)) + if (logger.IsLogEnabled(LogLevel.Debug)) { - _logger.Debug("Daily scav quests still locked, Intel center not built"); + logger.Debug("Daily scav quests still locked, Intel center not built"); } return false; @@ -830,9 +830,9 @@ public class RepeatableQuestController( if (questStatusInProfile.Status == QuestStatusEnum.AvailableForFinish) { questsToKeep.Add(activeQuest); - if (_logger.IsLogEnabled(LogLevel.Debug)) + if (logger.IsLogEnabled(LogLevel.Debug)) { - _logger.Debug( // TODO: this shouldn't happen, doesn't on live + logger.Debug( // TODO: this shouldn't happen, doesn't on live $"Keeping repeatable quest: {activeQuest.Id} in activeQuests since it is available to hand in" ); } @@ -841,7 +841,7 @@ public class RepeatableQuestController( } // Clean up quest-related counters being left in profile - _profileFixerService.RemoveDanglingConditionCounters(pmcData); + profileFixerService.RemoveDanglingConditionCounters(pmcData); // Remove expired quest from pmc.quest array pmcData.Quests = pmcData.Quests.Where(quest => quest.QId != activeQuest.Id).ToList(); @@ -878,12 +878,12 @@ public class RepeatableQuestController( // Add "any" to pickup quest pool questPool.Pool.Pickup.Locations[ELocationName.any] = ["any"]; - var eliminationConfig = _repeatableQuestHelper.GetEliminationConfigByPmcLevel( + var eliminationConfig = repeatableQuestHelper.GetEliminationConfigByPmcLevel( pmcLevel, repeatableConfig ); var targetsConfig = new ProbabilityObjectArray( - _cloner, + cloner, eliminationConfig.Targets ); @@ -929,7 +929,7 @@ public class RepeatableQuestController( { return new QuestTypePool { - Types = _cloner.Clone(repeatableConfig.Types)!, + Types = cloner.Clone(repeatableConfig.Types)!, Pool = new QuestPool { Exploration = new ExplorationPool @@ -959,20 +959,20 @@ public class RepeatableQuestController( var questCount = repeatableConfig.NumQuests; if (questCount == 0) { - _logger.Warning($"Repeatable: {repeatableConfig.Name} quests have a count of 0"); + logger.Warning($"Repeatable: {repeatableConfig.Name} quests have a count of 0"); } // Add elite bonus to daily quests if ( string.Equals(repeatableConfig.Name, "daily", StringComparison.OrdinalIgnoreCase) - && _profileHelper.HasEliteSkillLevel( + && profileHelper.HasEliteSkillLevel( SkillTypes.Charisma, fullProfile.CharacterData.PmcData ) ) // Elite charisma skill gives extra daily quest(s) { - questCount += _databaseService + questCount += databaseService .GetGlobals() .Configuration.SkillsSettings.Charisma.BonusSettings.EliteBonusSettings.RepeatableQuestExtraCount.GetValueOrDefault( 0 diff --git a/Libraries/SPTarkov.Server.Core/Controllers/TraderController.cs b/Libraries/SPTarkov.Server.Core/Controllers/TraderController.cs index 945027c6..a8e27365 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/TraderController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/TraderController.cs @@ -1,4 +1,5 @@ using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Generators; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Eft.Common.Tables; @@ -43,20 +44,20 @@ public class TraderController( var traders = databaseService.GetTraders(); foreach (var (traderId, trader) in traders) { - switch (traderId) + if (traderId == Traders.LIGHTHOUSEKEEPER || traderId == "ragfair") { - case "ragfair": - case Traders.LIGHTHOUSEKEEPER: - continue; - case Traders.FENCE: - fenceBaseAssortGenerator.GenerateFenceBaseAssorts(); - fenceService.GenerateFenceAssorts(); + continue; + } - continue; + if (traderId == Traders.FENCE) + { + fenceBaseAssortGenerator.GenerateFenceBaseAssorts(); + fenceService.GenerateFenceAssorts(); + continue; } // Adjust price by traderPriceMultiplier config property - if (TraderConfig.TraderPriceMultiplier != 1) + if (!TraderConfig.TraderPriceMultiplier.Approx(1, 0.001)) { AdjustTraderItemPrices(trader, TraderConfig.TraderPriceMultiplier); } @@ -101,19 +102,19 @@ public class TraderController( { foreach (var (traderId, trader) in databaseService.GetTables().Traders) { - switch (traderId) + if (traderId == Traders.LIGHTHOUSEKEEPER) { - case Traders.LIGHTHOUSEKEEPER: - continue; - case Traders.FENCE: - { - if (fenceService.NeedsPartialRefresh()) - { - fenceService.GenerateFenceAssorts(); - } + continue; + } - continue; + if (traderId == Traders.FENCE) + { + if (fenceService.NeedsPartialRefresh()) + { + fenceService.GenerateFenceAssorts(); } + + continue; } if (!traderAssortHelper.TraderAssortsHaveExpired(traderId)) diff --git a/Libraries/SPTarkov.Server.Core/Extensions/MathExtensions.cs b/Libraries/SPTarkov.Server.Core/Extensions/MathExtensions.cs index a1f489b8..7770f888 100644 --- a/Libraries/SPTarkov.Server.Core/Extensions/MathExtensions.cs +++ b/Libraries/SPTarkov.Server.Core/Extensions/MathExtensions.cs @@ -55,5 +55,29 @@ { return values.Select(v => v * factor); } + + /// + /// Helper to determine if one double is approx equal to another double + /// + /// Value to check + /// Target value + /// Error value + /// True if value is approx target within the error range + public static bool Approx(this double value, double target, double error = 0.001d) + { + return Math.Abs(value - target) <= error; + } + + /// + /// Helper to determine if one float is approx equal to another float + /// + /// Value to check + /// Target value + /// Error value + /// True if value is approx target within the error range + public static bool Approx(this float value, float target, float error = 0.001f) + { + return Math.Abs(value - target) <= error; + } } } diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs index 5d831b82..f4b75f96 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs @@ -46,7 +46,7 @@ public class CompletionQuestGenerator( public RepeatableQuest? Generate( string sessionId, int pmcLevel, - string traderId, + MongoId traderId, QuestTypePool questTypePool, RepeatableQuestConfig repeatableConfig ) @@ -269,8 +269,8 @@ public class CompletionQuestGenerator( // Filter and concatenate the arrays according to current player level var itemIdsBlacklisted = itemBlacklist - .Where(p => p.MinPlayerLevel <= pmcLevel) - .SelectMany(x => x.ItemIds) + .Where(blacklist => blacklist.MinPlayerLevel <= pmcLevel) + .SelectMany(blacklist => blacklist.ItemIds) .ToHashSet(); //.Aggregate(List , (a, p) => a.Concat(p.ItemIds) ); var filteredSelection = itemSelection diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/EliminationQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/EliminationQuestGenerator.cs index 27770b58..18ccf319 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/EliminationQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/EliminationQuestGenerator.cs @@ -1,5 +1,6 @@ using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Helpers; +using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; @@ -54,8 +55,8 @@ public class EliminationQuestGenerator( Dictionary> LocationsConfig, ProbabilityObjectArray TargetsConfig, ProbabilityObjectArray> BodyPartsConfig, - ProbabilityObjectArray> WeaponCategoryRequirementConfig, - ProbabilityObjectArray> WeaponRequirementConfig + ProbabilityObjectArray> WeaponCategoryRequirementConfig, + ProbabilityObjectArray> WeaponRequirementConfig ); /// @@ -73,7 +74,7 @@ public class EliminationQuestGenerator( public RepeatableQuest? Generate( string sessionId, int pmcLevel, - string traderId, + MongoId traderId, QuestTypePool questTypePool, RepeatableQuestConfig repeatableConfig ) @@ -213,7 +214,7 @@ public class EliminationQuestGenerator( } // Only allow a specific weapon requirement if a weapon category was not chosen - string? allowedWeapon = null; + MongoId? allowedWeapon = null; var generateWeaponRequirement = randomUtil.GetChance100( generationData.EliminationConfig.WeaponRequirementChance @@ -340,11 +341,11 @@ public class EliminationQuestGenerator( cloner, eliminationConfig.BodyParts ); - var weaponCategoryRequirementConfig = new ProbabilityObjectArray>( + var weaponCategoryRequirementConfig = new ProbabilityObjectArray>( cloner, eliminationConfig.WeaponCategoryRequirements ); - var weaponRequirementConfig = new ProbabilityObjectArray>( + var weaponRequirementConfig = new ProbabilityObjectArray>( cloner, eliminationConfig.WeaponRequirements ); @@ -667,7 +668,7 @@ public class EliminationQuestGenerator( /// /// Generation data /// Weapon to use - protected string? GenerateSpecificWeaponRequirement( + protected MongoId GenerateSpecificWeaponRequirement( EliminationQuestGenerationData generationData ) { @@ -681,7 +682,7 @@ public class EliminationQuestGenerator( logger.Error( localisationService.GetText("repeatable-elimination-specific-weapon-null") ); - return null; + return MongoId.Empty(); } var allowedWeapons = itemHelper.GetItemTplsOfBaseType(specificAllowedWeaponCategory[0]); diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs index 841ee557..dbe026e6 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs @@ -1,5 +1,6 @@ using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Helpers; +using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; @@ -50,7 +51,7 @@ public class ExplorationQuestGenerator( public RepeatableQuest? Generate( string sessionId, int pmcLevel, - string traderId, + MongoId traderId, QuestTypePool questTypePool, RepeatableQuestConfig repeatableConfig ) diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/IRepeatableQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/IRepeatableQuestGenerator.cs index 9196dab1..641c2398 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/IRepeatableQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/IRepeatableQuestGenerator.cs @@ -1,4 +1,5 @@ -using SPTarkov.Server.Core.Models.Eft.Common.Tables; +using SPTarkov.Server.Core.Models.Common; +using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Spt.Repeatable; @@ -9,7 +10,7 @@ public interface IRepeatableQuestGenerator public RepeatableQuest? Generate( string sessionId, int pmcLevel, - string traderId, + MongoId 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 index e2e6408c..0e48e8e5 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/PickupQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/PickupQuestGenerator.cs @@ -1,5 +1,6 @@ using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Helpers; +using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; @@ -27,7 +28,7 @@ public class PickupQuestGenerator( public RepeatableQuest? Generate( string sessionId, int pmcLevel, - string traderId, + MongoId traderId, QuestTypePool questTypePool, RepeatableQuestConfig repeatableConfig ) diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/RepeatableQuestRewardGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/RepeatableQuestRewardGenerator.cs index d656f835..294ae605 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/RepeatableQuestRewardGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/RepeatableQuestRewardGenerator.cs @@ -62,7 +62,7 @@ public class RepeatableQuestRewardGenerator( public QuestRewards? GenerateReward( int pmcLevel, double difficulty, - string traderId, + MongoId traderId, RepeatableQuestConfig repeatableConfig, BaseQuestConfig eliminationConfig, List? rewardTplBlacklist = null @@ -707,7 +707,7 @@ public class RepeatableQuestRewardGenerator( /// If generated Item is found in raid, default True /// Object of "Reward"-item-type protected Reward GenerateItemReward( - string tpl, + MongoId tpl, double count, int index, bool foundInRaid = true @@ -740,13 +740,14 @@ public class RepeatableQuestRewardGenerator( return questRewardItem; } - protected Reward GetMoneyReward(string traderId, double rewardRoubles, int rewardIndex) + protected Reward GetMoneyReward(MongoId traderId, double rewardRoubles, int rewardIndex) { // Determine currency based on trader // PK and Fence use Euros, everyone else is Roubles - var currency = traderId is Traders.PEACEKEEPER or Traders.FENCE - ? Money.EUROS - : Money.ROUBLES; + var currency = + traderId == Traders.PEACEKEEPER || traderId == Traders.FENCE + ? Money.EUROS + : Money.ROUBLES; // Convert reward amount to Euros if necessary var rewardAmountToGivePlayer = @@ -766,7 +767,7 @@ public class RepeatableQuestRewardGenerator( /// - Have a price greater than 0 /// /// Config - /// ID of trader who will give reward to player + /// ID of trader who will give reward to player /// List of rewardable items [[_tpl, itemTemplate],...] public List GetRewardableItems( RepeatableQuestConfig repeatableQuestConfig, @@ -821,7 +822,7 @@ public class RepeatableQuestRewardGenerator( string tpl, HashSet itemTplBlacklist, HashSet itemTypeBlacklist, - List? itemBaseWhitelist = null + List? itemBaseWhitelist = null ) { // Return early if not valid item to give as reward diff --git a/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs index 995703f0..9eb9aa87 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs @@ -1,4 +1,5 @@ using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; @@ -14,6 +15,7 @@ namespace SPTarkov.Server.Core.Helpers; public class RepeatableQuestHelper( ISptLogger logger, DatabaseService databaseService, + ServerLocalisationService serverLocalisationService, HashUtil hashUtil, ICloner cloner, ConfigServer configServer @@ -43,7 +45,7 @@ public class RepeatableQuestHelper( /// Side to get the templates for /// /// - public Dictionary GetRepeatableQuestTemplatesByGroup(PlayerGroup playerGroup) + public Dictionary GetRepeatableQuestTemplatesByGroup(PlayerGroup playerGroup) { var templates = QuestConfig.RepeatableQuestTemplates; @@ -107,7 +109,7 @@ public class RepeatableQuestHelper( /// public RepeatableQuest? GenerateRepeatableTemplate( RepeatableQuestType type, - string traderId, + MongoId traderId, PlayerGroup playerGroup, string sessionId ) @@ -116,8 +118,12 @@ public class RepeatableQuestHelper( if (questData is null) { - // TODO: Localize me! - logger.Error($"No repeatable quest template found for type {type}"); + logger.Error( + serverLocalisationService.GetText( + "repeatable-quest_helper_template_not_found", + type + ) + ); return null; } @@ -128,15 +134,20 @@ public class RepeatableQuestHelper( if (templateName is null) { - // TODO: Localize me! - logger.Error($"Could not resolve template name for {type}"); + logger.Error( + serverLocalisationService.GetText( + "repeatable-quest_helper_template_name_not_found", + type + ) + ); return null; } questData.TemplateId = typeIds[templateName]; // Force REF templates to use prapors ID - solves missing text issue - var desiredTraderId = traderId == Traders.REF ? Traders.PRAPOR : traderId; + // TODO: Get rid of this new mongoid generation, needs handled in `Traders` but can't be done right now. + var desiredTraderId = traderId == Traders.REF ? new MongoId(Traders.PRAPOR) : traderId; /* in locale, these id correspond to the text of quests template ids -pmc : Elimination = 616052ea3054fc0e2c24ce6e / Completion = 61604635c725987e815b1a46 / Exploration = 616041eb031af660100c9967 @@ -185,8 +196,9 @@ public class RepeatableQuestHelper( if (questData.QuestStatus is null) { - // TODO: Localize me! - logger.Error($"No quest status found for type {type}"); + logger.Error( + serverLocalisationService.GetText("repeatable-quest_helper_no_status", type) + ); return null; } @@ -206,8 +218,9 @@ public class RepeatableQuestHelper( { if (!QuestConfig.LocationIdMap.TryGetValue(locationKey, out var locationId)) { - // TODO - localize me! - logger.Error($"No location in LocationIdMap found for key {locationKey}"); + logger.Error( + serverLocalisationService.GetText("repeatable-quest_helper_no_loc_id", locationKey) + ); return null; } diff --git a/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs b/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs index 7aa1e4c6..b547fedb 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; using SPTarkov.Server.Core.Extensions; namespace SPTarkov.Server.Core.Models.Common; @@ -7,7 +8,13 @@ public readonly partial struct MongoId : IEquatable { private readonly string _stringId; - public MongoId(string id) + public MongoId( + string id, + // TODO: TEMPORARY REMOVE ME WHEN DONE!!!! + [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string methodName = "", + [CallerLineNumber] int callerLineNumber = 0 + ) { // This is temporary, otherwise item buying is broken as when LINQ searches for string id's it's possible null is passed if (id == null) @@ -18,13 +25,15 @@ public readonly partial struct MongoId : IEquatable if (id.Length != 24) { // TODO: Items.json root item has an empty parentId property - Console.WriteLine($"Critical MongoId error: Incorrect length. id: {id}"); + Console.WriteLine( + $"Critical MongoId error [{callerFilePath}::{methodName} L{callerLineNumber}]: Incorrect length. id: {id}" + ); } if (!IsValidMongoId(id)) { Console.WriteLine( - $"Critical MongoId error: Incorrect format. Must be a hexadecimal [a-f0-9] of 24 characters. id: {id}" + $"Critical MongoId error [{callerFilePath}::{methodName} L{callerLineNumber}]: Incorrect format. Must be a hexadecimal [a-f0-9] of 24 characters. id: {id}" ); } diff --git a/Libraries/SPTarkov.Server.Core/Models/Enums/Traders.cs b/Libraries/SPTarkov.Server.Core/Models/Enums/Traders.cs index ea2fd28b..67b1ee19 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Enums/Traders.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Enums/Traders.cs @@ -1,16 +1,18 @@ +using SPTarkov.Server.Core.Models.Common; + namespace SPTarkov.Server.Core.Models.Enums; public static class Traders { - public const string PRAPOR = "54cb50c76803fa8b248b4571"; - public const string THERAPIST = "54cb57776803fa99248b456e"; - public const string FENCE = "579dc571d53a0658a154fbec"; - public const string SKIER = "58330581ace78e27b8b10cee"; - public const string PEACEKEEPER = "5935c25fb3acc3127c3d8cd9"; - public const string MECHANIC = "5a7c2eca46aef81a7ca2145d"; - public const string RAGMAN = "5ac3b934156ae10c4430e83c"; - public const string JAEGER = "5c0647fdd443bc2504c2d371"; - public const string LIGHTHOUSEKEEPER = "638f541a29ffd1183d187f57"; - public const string BTR = "656f0f98d80a697f855d34b1"; - public const string REF = "6617beeaa9cfa777ca915b7c"; + public static MongoId PRAPOR = new("54cb50c76803fa8b248b4571"); + public static MongoId THERAPIST = new("54cb57776803fa99248b456e"); + public static MongoId FENCE = new("579dc571d53a0658a154fbec"); + public static MongoId SKIER = new("58330581ace78e27b8b10cee"); + public static MongoId PEACEKEEPER = new("5935c25fb3acc3127c3d8cd9"); + public static MongoId MECHANIC = new("5a7c2eca46aef81a7ca2145d"); + public static MongoId RAGMAN = new("5ac3b934156ae10c4430e83c"); + public static MongoId JAEGER = new("5c0647fdd443bc2504c2d371"); + public static MongoId LIGHTHOUSEKEEPER = new("638f541a29ffd1183d187f57"); + public static MongoId BTR = new("656f0f98d80a697f855d34b1"); + public static MongoId REF = new("6617beeaa9cfa777ca915b7c"); } diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs index 1e727878..78797caa 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs @@ -21,25 +21,25 @@ public record QuestConfig : BaseConfig /// Collection of quests by id only available to usec /// [JsonPropertyName("usecOnlyQuests")] - public required HashSet UsecOnlyQuests { get; set; } + public required HashSet UsecOnlyQuests { get; set; } /// /// Collection of quests by id only available to bears /// [JsonPropertyName("bearOnlyQuests")] - public required HashSet BearOnlyQuests { get; set; } + public required HashSet BearOnlyQuests { get; set; } /// /// Quests that the keyed game version do not see/access /// [JsonPropertyName("profileBlacklist")] - public required Dictionary> ProfileBlacklist { get; set; } + public required Dictionary> ProfileBlacklist { get; set; } /// /// key=questid, gameversions that can see/access quest /// [JsonPropertyName("profileWhitelist")] - public required Dictionary> ProfileWhitelist { get; set; } + public required Dictionary> ProfileWhitelist { get; set; } /// /// Holds repeatable quest template ids for pmc's and scav's @@ -57,7 +57,7 @@ public record QuestConfig : BaseConfig /// Collection of event quest data keyed by quest id. /// [JsonPropertyName("eventQuests")] - public required Dictionary EventQuests { get; set; } + public required Dictionary EventQuests { get; set; } /// /// List of repeatable quest configs for; daily, weekly, and daily scav. @@ -82,14 +82,14 @@ public record RepeatableQuestTemplates /// Keys: elimination, completion, exploration /// [JsonPropertyName("pmc")] - public required Dictionary Pmc { get; set; } + public required Dictionary Pmc { get; set; } /// /// Scav repeatable quest template ids keyed by type of quest /// Keys: elimination, completion, exploration, pickup /// [JsonPropertyName("scav")] - public required Dictionary Scav { get; set; } + public required Dictionary Scav { get; set; } } public record EventQuestData @@ -138,7 +138,7 @@ public record RepeatableQuestConfig /// Id for type of repeatable quest /// [JsonPropertyName("id")] - public required string Id { get; set; } + public required MongoId Id { get; set; } /// /// Human-readable name for repeatable quest type @@ -314,7 +314,7 @@ public record TraderWhitelist /// Trader Id /// [JsonPropertyName("traderId")] - public required string TraderId { get; set; } + public required MongoId TraderId { get; set; } /// /// Human-readable name @@ -332,7 +332,7 @@ public record TraderWhitelist /// Item categories that the reward can be /// [JsonPropertyName("rewardBaseWhitelist")] - public required List RewardBaseWhitelist { get; set; } + public required List RewardBaseWhitelist { get; set; } /// /// Can this reward be a weapon? @@ -614,14 +614,14 @@ public record EliminationConfig : BaseQuestConfig /// [JsonPropertyName("weaponCategoryRequirements")] public required List< - ProbabilityObject> + ProbabilityObject> > WeaponCategoryRequirements { get; set; } /// /// If a weapon requirement is chosen, pick from these weapons /// [JsonPropertyName("weaponRequirements")] - public required List>> WeaponRequirements { get; set; } + public required List>> WeaponRequirements { get; set; } } public record BaseQuestConfig diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 1d9882d3..7e7daaf5 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -2,6 +2,7 @@ using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Generators; using SPTarkov.Server.Core.Helpers; +using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Eft.Match; @@ -580,7 +581,7 @@ public class LocationLifecycleService pmcData.CarExtractCounts[extractName] ); - const string fenceId = Traders.FENCE; + var fenceId = Traders.FENCE; pmcData.TradersInfo[fenceId].Standing = newFenceStanding; // Check if new standing has leveled up trader @@ -618,7 +619,7 @@ public class LocationLifecycleService pmcData.CoopExtractCounts[extractName] ); - const string fenceId = Traders.FENCE; + var fenceId = Traders.FENCE; pmcData.TradersInfo[fenceId].Standing = newFenceStanding; // Check if new standing has leveled up trader @@ -649,7 +650,7 @@ public class LocationLifecycleService double extractCount ) { - const string fenceId = Traders.FENCE; + var fenceId = Traders.FENCE; var fenceStanding = pmcData.TradersInfo[fenceId].Standing; // get standing after taking extract x times, x.xx format, gain from extract can be no smaller than 0.01 @@ -946,7 +947,7 @@ public class LocationLifecycleService // Must occur AFTER experience is set and stats copied over serverPmcProfile.Stats.Eft.TotalSessionExperience = 0; - const string fenceId = Traders.FENCE; + var fenceId = Traders.FENCE; // Clamp fence standing var currentFenceStanding = postRaidProfile.TradersInfo[fenceId].Standing;