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