From 445243aad5408a030ca73bd05e9e8123757ee860 Mon Sep 17 00:00:00 2001 From: Cj <161484149+CJ-SPT@users.noreply.github.com> Date: Fri, 20 Jun 2025 03:48:12 -0400 Subject: [PATCH] Task: Quest config changes Part 1 (#407) * Remove nullable and add properties, make side use an enum instead of a string. * remove double semi-colon * fix comment --- .../SPT_Data/configs/quest.json | 18 +- .../Controllers/RepeatableQuestController.cs | 10 +- .../Generators/RepeatableQuestGenerator.cs | 42 ++-- .../Helpers/QuestHelper.cs | 6 +- .../Helpers/RepeatableQuestHelper.cs | 27 ++- .../Models/Enums/PlayerGroup.cs | 10 + .../Models/Spt/Config/QuestConfig.cs | 204 ++++++++++++------ 7 files changed, 207 insertions(+), 110 deletions(-) create mode 100644 Libraries/SPTarkov.Server.Core/Models/Enums/PlayerGroup.cs diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json index b1ee8da3..9bfe29f1 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json @@ -25,17 +25,17 @@ "profileWhitelist": { "666314b4d7f171c4c20226c3": ["edge_of_darkness"] }, - "questTemplateIds": { + "repeatableQuestTemplateIds": { "pmc": { - "elimination": "616052ea3054fc0e2c24ce6e", - "completion": "61604635c725987e815b1a46", - "exploration": "616041eb031af660100c9967" + "Elimination": "616052ea3054fc0e2c24ce6e", + "Completion": "61604635c725987e815b1a46", + "Exploration": "616041eb031af660100c9967" }, "scav": { - "elimination": "62825ef60e88d037dc1eb428", - "completion": "628f588ebb558574b2260fe5", - "exploration": "62825ef60e88d037dc1eb42c", - "pickup": "628f588ebb558574b2260fe5" + "Elimination": "62825ef60e88d037dc1eb428", + "Completion": "628f588ebb558574b2260fe5", + "Exploration": "62825ef60e88d037dc1eb42c", + "Pickup": "628f588ebb558574b2260fe5" } }, "showNonSeasonalEventQuests": false, @@ -1891,6 +1891,7 @@ "rewardAmmoStackMinSize": 5, "freeChangesAvailable": 0, "freeChanges": 0, + "keepDailyQuestTypeOnReplacement": false, "standingChangeCost": [0, 0.01, 0.01] }, { @@ -2314,6 +2315,7 @@ "rewardAmmoStackMinSize": 5, "freeChangesAvailable": 0, "freeChanges": 0, + "keepDailyQuestTypeOnReplacement": false, "standingChangeCost": [0, 0.01] } ], diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs index a10d483a..7d80bb15 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs @@ -184,7 +184,7 @@ public class RepeatableQuestController( } // Add newly generated quest to daily/weekly/scav type array - newRepeatableQuest.Side = repeatableConfig.Side; + newRepeatableQuest.Side = Enum.GetName(repeatableConfig.Side); repeatablesOfTypeInProfile.ActiveQuests.Add(newRepeatableQuest); if (_logger.IsLogEnabled(LogLevel.Debug)) @@ -568,7 +568,7 @@ public class RepeatableQuestController( break; } - quest.Side = repeatableConfig.Side; + quest.Side = Enum.GetName(repeatableConfig.Side); generatedRepeatables.ActiveQuests.Add(quest); } @@ -674,7 +674,7 @@ public class RepeatableQuestController( { // PMC and daily quests not unlocked yet if ( - repeatableConfig.Side == "Pmc" + repeatableConfig.Side == PlayerGroup.Pmc && !PlayerHasDailyPmcQuestsUnlocked(pmcData, repeatableConfig) ) { @@ -682,7 +682,7 @@ public class RepeatableQuestController( } // Scav and daily quests not unlocked yet - if (repeatableConfig.Side == "Scav" && !PlayerHasDailyScavQuestsUnlocked(pmcData)) + if (repeatableConfig.Side == PlayerGroup.Scav && !PlayerHasDailyScavQuestsUnlocked(pmcData)) { if (_logger.IsLogEnabled(LogLevel.Debug)) { @@ -875,7 +875,7 @@ public class RepeatableQuestController( /// Quest count protected int GetQuestCount(RepeatableQuestConfig repeatableConfig, SptProfile fullProfile) { - var questCount = repeatableConfig.NumQuests.GetValueOrDefault(0); + var questCount = repeatableConfig.NumQuests; if (questCount == 0) { _logger.Warning($"Repeatable: {repeatableConfig.Name} quests have a count of 0"); diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGenerator.cs index be7ade2d..90048e79 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGenerator.cs @@ -1046,13 +1046,13 @@ public class RepeatableQuestGenerator( /// Filter a maps exits to just those for the desired side /// /// Map id (e.g. factory4_day) - /// Scav/Pmc + /// Pmc/Scav /// List of Exit objects - protected List GetLocationExitsForSide(string locationKey, string playerSide) + protected List GetLocationExitsForSide(string locationKey, PlayerGroup playerGroup) { var mapExtracts = _databaseService.GetLocation(locationKey.ToLower()).AllExtracts; - return mapExtracts.Where(exit => exit.Side == playerSide).ToList(); + return mapExtracts.Where(exit => exit.Side == Enum.GetName(playerGroup)).ToList(); } protected RepeatableQuest GeneratePickupQuest( @@ -1149,7 +1149,7 @@ public class RepeatableQuestGenerator( /// /// Quest type: "Elimination", "Completion" or "Extraction" /// Trader from which the quest will be provided - /// Scav daily or pmc daily/weekly quest + /// Scav daily or pmc daily/weekly quest /// /// Object which contains the base elements for repeatable quests of the requests type /// (needs to be filled with reward and conditions by called to make a valid quest) @@ -1157,7 +1157,7 @@ public class RepeatableQuestGenerator( protected RepeatableQuest GenerateRepeatableTemplate( string type, string traderId, - string side, + PlayerGroup playerGroup, string sessionId ) { @@ -1188,28 +1188,9 @@ public class RepeatableQuestGenerator( */ // Get template id from config based on side and type of quest - var typeIds = string.Equals(side, "pmc", StringComparison.OrdinalIgnoreCase) - ? _questConfig.QuestTemplateIds.Pmc - : _questConfig.QuestTemplateIds.Scav; + var typeIds = _repeatableQuestHelper.GetRepeatableQuestTemplatesByGroup(playerGroup); - var templateId = string.Empty; - switch (type) - { - case "Completion": - templateId = typeIds.Completion; - break; - case "Elimination": - templateId = typeIds.Elimination; - break; - case "Exploration": - templateId = typeIds.Exploration; - break; - case "Pickup": - templateId = typeIds.Pickup; - break; - } - - questClone.TemplateId = templateId; + questClone.TemplateId = typeIds[type]; // Force REF templates to use prapors ID - solves missing text issue var desiredTraderId = traderId == Traders.REF ? Traders.PRAPOR : traderId; @@ -1217,30 +1198,39 @@ public class RepeatableQuestGenerator( questClone.Name = questClone .Name.Replace("{traderId}", traderId) .Replace("{templateId}", questClone.TemplateId); + questClone.Note = questClone .Note.Replace("{traderId}", desiredTraderId) .Replace("{templateId}", questClone.TemplateId); + questClone.Description = questClone .Description.Replace("{traderId}", desiredTraderId) .Replace("{templateId}", questClone.TemplateId); + questClone.SuccessMessageText = questClone .SuccessMessageText.Replace("{traderId}", desiredTraderId) .Replace("{templateId}", questClone.TemplateId); + questClone.FailMessageText = questClone .FailMessageText.Replace("{traderId}", desiredTraderId) .Replace("{templateId}", questClone.TemplateId); + questClone.StartedMessageText = questClone .StartedMessageText.Replace("{traderId}", desiredTraderId) .Replace("{templateId}", questClone.TemplateId); + questClone.ChangeQuestMessageText = questClone .ChangeQuestMessageText.Replace("{traderId}", desiredTraderId) .Replace("{templateId}", questClone.TemplateId); + questClone.AcceptPlayerMessage = questClone .AcceptPlayerMessage.Replace("{traderId}", desiredTraderId) .Replace("{templateId}", questClone.TemplateId); + questClone.DeclinePlayerMessage = questClone .DeclinePlayerMessage.Replace("{traderId}", desiredTraderId) .Replace("{templateId}", questClone.TemplateId); + questClone.CompletePlayerMessage = questClone .CompletePlayerMessage.Replace("{traderId}", desiredTraderId) .Replace("{templateId}", questClone.TemplateId); diff --git a/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs index 2d7711f8..a31ff457 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs @@ -513,7 +513,7 @@ public class QuestHelper( // Should non-season event quests be shown to player if ( - !(_questConfig.ShowNonSeasonalEventQuests ?? false) + !_questConfig.ShowNonSeasonalEventQuests && _seasonalEventService.IsQuestRelatedToEvent(questId, SeasonalEventType.None) ) { @@ -1173,10 +1173,10 @@ public class QuestHelper( { if (!_questConfig.MailRedeemTimeHours.TryGetValue(pmcData.Info.GameVersion, out var hours)) { - return _questConfig.MailRedeemTimeHours["default"] ?? 48; + return _questConfig.MailRedeemTimeHours["default"]; } - return hours ?? 48; + return hours; } /// diff --git a/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs index 7e451921..96c81b62 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs @@ -1,12 +1,19 @@ using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Utils; +using SPTarkov.Server.Core.Servers; namespace SPTarkov.Server.Core.Helpers; [Injectable] -public class RepeatableQuestHelper(ISptLogger _logger) +public class RepeatableQuestHelper( + ISptLogger _logger, + ConfigServer _configServer + ) { + protected QuestConfig _questConfig = _configServer.GetConfig(); + /// /// Get the relevant elimination config based on the current players PMC level /// @@ -22,4 +29,22 @@ public class RepeatableQuestHelper(ISptLogger _logger) pmcLevel >= x.LevelRange.Min && pmcLevel <= x.LevelRange.Max ); } + + /// + /// Returns the repeatable template ids for the provided side + /// + /// Side to get the templates for + /// + /// + public Dictionary? GetRepeatableQuestTemplatesByGroup(PlayerGroup playerGroup) + { + var templates = _questConfig.RepeatableQuestTemplates; + + return playerGroup switch + { + PlayerGroup.Pmc => templates.Pmc, + PlayerGroup.Scav => templates.Scav, + _ => throw new ArgumentOutOfRangeException(nameof(playerGroup), playerGroup, null) + }; + } } diff --git a/Libraries/SPTarkov.Server.Core/Models/Enums/PlayerGroup.cs b/Libraries/SPTarkov.Server.Core/Models/Enums/PlayerGroup.cs new file mode 100644 index 00000000..964f0e71 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Models/Enums/PlayerGroup.cs @@ -0,0 +1,10 @@ +using SPTarkov.Server.Core.Utils.Json.Converters; + +namespace SPTarkov.Server.Core.Models.Enums; + +[EftEnumConverter] +public enum PlayerGroup +{ + Pmc, + Scav +} diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs index 685c68d3..e0ff9dbf 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs @@ -15,73 +15,81 @@ public record QuestConfig : BaseConfig /// Hours to get/redeem items from quest mail keyed by profile type /// [JsonPropertyName("mailRedeemTimeHours")] - public Dictionary? MailRedeemTimeHours { get; set; } - - [JsonPropertyName("questTemplateIds")] - public PlayerTypeQuestIds? QuestTemplateIds { get; set; } + public required Dictionary MailRedeemTimeHours { get; set; } /// - /// Show non-seasonal quests be shown to player + /// Collection of quests by id only available to usec /// - [JsonPropertyName("showNonSeasonalEventQuests")] - public bool? ShowNonSeasonalEventQuests { get; set; } - - [JsonPropertyName("eventQuests")] - public Dictionary? EventQuests { get; set; } - - [JsonPropertyName("repeatableQuests")] - public List? RepeatableQuests { get; set; } - - [JsonPropertyName("locationIdMap")] - public Dictionary? LocationIdMap { get; set; } - - [JsonPropertyName("bearOnlyQuests")] - public HashSet? BearOnlyQuests { get; set; } - [JsonPropertyName("usecOnlyQuests")] - public 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; } /// /// Quests that the keyed game version do not see/access /// [JsonPropertyName("profileBlacklist")] - public Dictionary>? ProfileBlacklist { get; set; } + public required Dictionary> ProfileBlacklist { get; set; } /// /// key=questid, gameversions that can see/access quest /// [JsonPropertyName("profileWhitelist")] - public Dictionary>? ProfileWhitelist { get; set; } + public required Dictionary> ProfileWhitelist { get; set; } + + /// + /// Holds repeatable quest template ids for pmc's and scav's + /// + [JsonPropertyName("repeatableQuestTemplateIds")] + public required RepeatableQuestTemplates RepeatableQuestTemplates { get; set; } + + /// + /// Show non-seasonal quests be shown to players + /// + [JsonPropertyName("showNonSeasonalEventQuests")] + public required bool ShowNonSeasonalEventQuests { get; set; } + + /// + /// Collection of event quest data keyed by quest id. + /// + [JsonPropertyName("eventQuests")] + public required Dictionary EventQuests { get; set; } + + /// + /// List of repeatable quest configs for; daily, weekly, and daily scav. + /// + [JsonPropertyName("repeatableQuests")] + public required List RepeatableQuests { get; set; } + + /// + /// Maps internal map names to their mongoId: Key - internal :: val - Mongoid + /// + [JsonPropertyName("locationIdMap")] + public required Dictionary LocationIdMap { get; set; } } -public record PlayerTypeQuestIds +public record RepeatableQuestTemplates { [JsonExtensionData] public Dictionary ExtensionData { get; set; } + /// + /// Pmc repeatable quest template ids keyed by type of quest + /// Keys: elimination, completion, exploration + /// [JsonPropertyName("pmc")] - public QuestTypeIds? 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 QuestTypeIds? Scav { get; set; } -} - -public record QuestTypeIds -{ - [JsonExtensionData] - public Dictionary ExtensionData { get; set; } - - [JsonPropertyName("elimination")] - public string? Elimination { get; set; } - - [JsonPropertyName("completion")] - public string? Completion { get; set; } - - [JsonPropertyName("exploration")] - public string? Exploration { get; set; } - - [JsonPropertyName("pickup")] - public string? Pickup { get; set; } + public required Dictionary Scav { get; set; } } public record EventQuestData @@ -89,21 +97,36 @@ public record EventQuestData [JsonExtensionData] public Dictionary ExtensionData { get; set; } + /// + /// Name of the event quest + /// [JsonPropertyName("name")] - public string? Name { get; set; } + public required string Name { get; set; } + /// + /// Season to which this quest belongs + /// [JsonPropertyName("season")] - public SeasonalEventType? Season { get; set; } + public required SeasonalEventType Season { get; set; } + /// + /// Start timestamp + /// [JsonPropertyName("startTimestamp")] - public long? StartTimestamp { get; set; } + public required long StartTimestamp { get; set; } + /// + /// End timestamp + /// [JsonPropertyName("endTimestamp")] [JsonConverter(typeof(StringToNumberFactoryConverter))] - public long? EndTimestamp { get; set; } + public required long EndTimestamp { get; set; } + /// + /// Is this quest part of a yearly event, ex: Christmas + /// [JsonPropertyName("yearly")] - public bool? Yearly { get; set; } + public required bool Yearly { get; set; } } public record RepeatableQuestConfig @@ -111,68 +134,115 @@ public record RepeatableQuestConfig [JsonExtensionData] public Dictionary ExtensionData { get; set; } + /// + /// Id for type of repeatable quest + /// [JsonPropertyName("id")] - public string? Id { get; set; } + public required string Id { get; set; } + /// + /// Human-readable name for repeatable quest type + /// [JsonPropertyName("name")] - public string? Name { get; set; } + public required string Name { get; set; } + /// + /// Side this config belongs to. Note: Random not implemented, do not use! + /// [JsonPropertyName("side")] - public string? Side { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter))] + public required PlayerGroup Side { get; set; } + /// + /// Types of tasks this config can generate; ex: Elimination + /// [JsonPropertyName("types")] - public List? Types { get; set; } + public required List Types { get; set; } + /// + /// How long does the task stay active for after accepting it + /// [JsonPropertyName("resetTime")] - public long? ResetTime { get; set; } + public required long ResetTime { get; set; } + /// + /// How many quests should we provide per ResetTime + /// [JsonPropertyName("numQuests")] - public int? NumQuests { get; set; } + public required int NumQuests { get; set; } + /// + /// Min player level required to receive a quest from this config + /// [JsonPropertyName("minPlayerLevel")] - public int? MinPlayerLevel { get; set; } + public required int MinPlayerLevel { get; set; } + /// + /// Reward scaling config + /// [JsonPropertyName("rewardScaling")] - public RewardScaling? RewardScaling { get; set; } + public required RewardScaling RewardScaling { get; set; } + /// + /// Location map + /// [JsonPropertyName("locations")] - public Dictionary>? Locations { get; set; } + public required Dictionary> Locations { get; set; } + /// + /// Traders that are allowed to generate tasks from this config. + /// Includes quest types, reward whitelist, and whether rewards can be weapons. + /// [JsonPropertyName("traderWhitelist")] - public List? TraderWhitelist { get; set; } + public required List TraderWhitelist { get; set; } + /// + /// Quest config, holds information on how a task should be generated + /// [JsonPropertyName("questConfig")] - public RepeatableQuestTypesConfig? QuestConfig { get; set; } + public RepeatableQuestTypesConfig QuestConfig { get; set; } /// /// Item base types to block when generating rewards /// [JsonPropertyName("rewardBaseTypeBlacklist")] - public HashSet? RewardBaseTypeBlacklist { get; set; } + public required HashSet RewardBaseTypeBlacklist { get; set; } /// /// Item tplIds to ignore when generating rewards /// [JsonPropertyName("rewardBlacklist")] - public HashSet? RewardBlacklist { get; set; } + public required HashSet RewardBlacklist { get; set; } + /// + /// Minimum stack size that an ammo reward should be generated with + /// [JsonPropertyName("rewardAmmoStackMinSize")] - public int? RewardAmmoStackMinSize { get; set; } + public required int RewardAmmoStackMinSize { get; set; } + /// + /// How many free task changes are available from this config + /// [JsonPropertyName("freeChangesAvailable")] - public int? FreeChangesAvailable { get; set; } + public required int FreeChangesAvailable { get; set; } + /// + /// How many free task changes remain from this config + /// [JsonPropertyName("freeChanges")] - public int? FreeChanges { get; set; } + public required int FreeChanges { get; set; } + /// + /// Should the task replacement category be the same as the one its replacing + /// [JsonPropertyName("keepDailyQuestTypeOnReplacement")] - public bool? KeepDailyQuestTypeOnReplacement { get; set; } + public required bool KeepDailyQuestTypeOnReplacement { get; set; } /// /// Reputation standing price for replacing a repeatable /// [JsonPropertyName("standingChangeCost")] - public IList? StandingChangeCost { get; set; } + public required IList StandingChangeCost { get; set; } } public record RewardScaling