Repeatable quest generation MongoID conversion (#439)

* Update repeatable quest generation for mongoid, add new server localizations, switch `Traders` to MongoId

* Give error default value
This commit is contained in:
Cj
2025-07-02 07:44:49 -04:00
committed by GitHub
parent abab349a0c
commit 371c9d58f0
15 changed files with 222 additions and 163 deletions
@@ -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",
@@ -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<RepeatableQuestChangeRequest> _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<RepeatableQuestChangeRequest> 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<string> _questTypes = ["PickUp", "Exploration", "Elimination"];
protected readonly QuestConfig QuestConfig = _configServer.GetConfig<QuestConfig>();
protected readonly QuestConfig QuestConfig = configServer.GetConfig<QuestConfig>();
/// <summary>
/// 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<PmcDataRepeatableQuest> GetClientRepeatableQuests(string sessionID)
{
var returnData = new List<PmcDataRepeatableQuest>();
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<string, BossInfo>(
_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
@@ -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))
@@ -55,5 +55,29 @@
{
return values.Select(v => v * factor);
}
/// <summary>
/// Helper to determine if one double is approx equal to another double
/// </summary>
/// <param name="value">Value to check</param>
/// <param name="target">Target value</param>
/// <param name="error">Error value</param>
/// <returns>True if value is approx target within the error range</returns>
public static bool Approx(this double value, double target, double error = 0.001d)
{
return Math.Abs(value - target) <= error;
}
/// <summary>
/// Helper to determine if one float is approx equal to another float
/// </summary>
/// <param name="value">Value to check</param>
/// <param name="target">Target value</param>
/// <param name="error">Error value</param>
/// <returns>True if value is approx target within the error range</returns>
public static bool Approx(this float value, float target, float error = 0.001f)
{
return Math.Abs(value - target) <= error;
}
}
}
@@ -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<ItemsBlacklist> , (a, p) => a.Concat(p.ItemIds) );
var filteredSelection = itemSelection
@@ -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<ELocationName, List<string>> LocationsConfig,
ProbabilityObjectArray<string, BossInfo> TargetsConfig,
ProbabilityObjectArray<string, List<string>> BodyPartsConfig,
ProbabilityObjectArray<string, List<string>> WeaponCategoryRequirementConfig,
ProbabilityObjectArray<string, List<string>> WeaponRequirementConfig
ProbabilityObjectArray<string, List<MongoId>> WeaponCategoryRequirementConfig,
ProbabilityObjectArray<string, List<MongoId>> WeaponRequirementConfig
);
/// <summary>
@@ -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<string, List<string>>(
var weaponCategoryRequirementConfig = new ProbabilityObjectArray<string, List<MongoId>>(
cloner,
eliminationConfig.WeaponCategoryRequirements
);
var weaponRequirementConfig = new ProbabilityObjectArray<string, List<string>>(
var weaponRequirementConfig = new ProbabilityObjectArray<string, List<MongoId>>(
cloner,
eliminationConfig.WeaponRequirements
);
@@ -667,7 +668,7 @@ public class EliminationQuestGenerator(
/// </summary>
/// <param name="generationData">Generation data</param>
/// <returns>Weapon to use</returns>
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]);
@@ -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
)
@@ -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
);
@@ -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
)
@@ -62,7 +62,7 @@ public class RepeatableQuestRewardGenerator(
public QuestRewards? GenerateReward(
int pmcLevel,
double difficulty,
string traderId,
MongoId traderId,
RepeatableQuestConfig repeatableConfig,
BaseQuestConfig eliminationConfig,
List<string>? rewardTplBlacklist = null
@@ -707,7 +707,7 @@ public class RepeatableQuestRewardGenerator(
/// <param name="foundInRaid"> If generated Item is found in raid, default True </param>
/// <returns> Object of "Reward"-item-type </returns>
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
/// </summary>
/// <param name="repeatableQuestConfig"> Config </param>
/// <param name="tradderId"> ID of trader who will give reward to player </param>
/// <param name="traderId"> ID of trader who will give reward to player </param>
/// <returns> List of rewardable items [[_tpl, itemTemplate],...] </returns>
public List<TemplateItem> GetRewardableItems(
RepeatableQuestConfig repeatableQuestConfig,
@@ -821,7 +822,7 @@ public class RepeatableQuestRewardGenerator(
string tpl,
HashSet<MongoId> itemTplBlacklist,
HashSet<MongoId> itemTypeBlacklist,
List<string>? itemBaseWhitelist = null
List<MongoId>? itemBaseWhitelist = null
)
{
// Return early if not valid item to give as reward
@@ -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<RepeatableQuestHelper> logger,
DatabaseService databaseService,
ServerLocalisationService serverLocalisationService,
HashUtil hashUtil,
ICloner cloner,
ConfigServer configServer
@@ -43,7 +45,7 @@ public class RepeatableQuestHelper(
/// <param name="playerGroup">Side to get the templates for</param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public Dictionary<string, string> GetRepeatableQuestTemplatesByGroup(PlayerGroup playerGroup)
public Dictionary<string, MongoId> GetRepeatableQuestTemplatesByGroup(PlayerGroup playerGroup)
{
var templates = QuestConfig.RepeatableQuestTemplates;
@@ -107,7 +109,7 @@ public class RepeatableQuestHelper(
/// </returns>
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;
}
@@ -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<MongoId>
{
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<MongoId>
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}"
);
}
@@ -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");
}
@@ -21,25 +21,25 @@ public record QuestConfig : BaseConfig
/// Collection of quests by id only available to usec
/// </summary>
[JsonPropertyName("usecOnlyQuests")]
public required HashSet<string> UsecOnlyQuests { get; set; }
public required HashSet<MongoId> UsecOnlyQuests { get; set; }
/// <summary>
/// Collection of quests by id only available to bears
/// </summary>
[JsonPropertyName("bearOnlyQuests")]
public required HashSet<string> BearOnlyQuests { get; set; }
public required HashSet<MongoId> BearOnlyQuests { get; set; }
/// <summary>
/// Quests that the keyed game version do not see/access
/// </summary>
[JsonPropertyName("profileBlacklist")]
public required Dictionary<string, HashSet<string>> ProfileBlacklist { get; set; }
public required Dictionary<string, HashSet<MongoId>> ProfileBlacklist { get; set; }
/// <summary>
/// key=questid, gameversions that can see/access quest
/// </summary>
[JsonPropertyName("profileWhitelist")]
public required Dictionary<string, HashSet<string>> ProfileWhitelist { get; set; }
public required Dictionary<MongoId, HashSet<string>> ProfileWhitelist { get; set; }
/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("eventQuests")]
public required Dictionary<string, EventQuestData> EventQuests { get; set; }
public required Dictionary<MongoId, EventQuestData> EventQuests { get; set; }
/// <summary>
/// List of repeatable quest configs for; daily, weekly, and daily scav.
@@ -82,14 +82,14 @@ public record RepeatableQuestTemplates
/// Keys: elimination, completion, exploration
/// </summary>
[JsonPropertyName("pmc")]
public required Dictionary<string, string> Pmc { get; set; }
public required Dictionary<string, MongoId> Pmc { get; set; }
/// <summary>
/// Scav repeatable quest template ids keyed by type of quest
/// Keys: elimination, completion, exploration, pickup
/// </summary>
[JsonPropertyName("scav")]
public required Dictionary<string, string> Scav { get; set; }
public required Dictionary<string, MongoId> Scav { get; set; }
}
public record EventQuestData
@@ -138,7 +138,7 @@ public record RepeatableQuestConfig
/// Id for type of repeatable quest
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; set; }
public required MongoId Id { get; set; }
/// <summary>
/// Human-readable name for repeatable quest type
@@ -314,7 +314,7 @@ public record TraderWhitelist
/// Trader Id
/// </summary>
[JsonPropertyName("traderId")]
public required string TraderId { get; set; }
public required MongoId TraderId { get; set; }
/// <summary>
/// Human-readable name
@@ -332,7 +332,7 @@ public record TraderWhitelist
/// Item categories that the reward can be
/// </summary>
[JsonPropertyName("rewardBaseWhitelist")]
public required List<string> RewardBaseWhitelist { get; set; }
public required List<MongoId> RewardBaseWhitelist { get; set; }
/// <summary>
/// Can this reward be a weapon?
@@ -614,14 +614,14 @@ public record EliminationConfig : BaseQuestConfig
/// </summary>
[JsonPropertyName("weaponCategoryRequirements")]
public required List<
ProbabilityObject<string, List<string>>
ProbabilityObject<string, List<MongoId>>
> WeaponCategoryRequirements { get; set; }
/// <summary>
/// If a weapon requirement is chosen, pick from these weapons
/// </summary>
[JsonPropertyName("weaponRequirements")]
public required List<ProbabilityObject<string, List<string>>> WeaponRequirements { get; set; }
public required List<ProbabilityObject<string, List<MongoId>>> WeaponRequirements { get; set; }
}
public record BaseQuestConfig
@@ -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;