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
This commit is contained in:
Cj
2025-06-20 03:48:12 -04:00
committed by GitHub
parent 1a6f5f779d
commit 445243aad5
7 changed files with 207 additions and 110 deletions
@@ -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]
}
],
@@ -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(
/// <returns>Quest count</returns>
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");
@@ -1046,13 +1046,13 @@ public class RepeatableQuestGenerator(
/// Filter a maps exits to just those for the desired side
/// </summary>
/// <param name="locationKey">Map id (e.g. factory4_day)</param>
/// <param name="playerSide">Scav/Pmc</param>
/// <param name="playerGroup">Pmc/Scav</param>
/// <returns>List of Exit objects</returns>
protected List<Exit> GetLocationExitsForSide(string locationKey, string playerSide)
protected List<Exit> 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(
/// </summary>
/// <param name="type">Quest type: "Elimination", "Completion" or "Extraction"</param>
/// <param name="traderId">Trader from which the quest will be provided</param>
/// <param name="side">Scav daily or pmc daily/weekly quest</param>
/// <param name="playerGroup">Scav daily or pmc daily/weekly quest</param>
/// <returns>
/// 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);
@@ -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;
}
/// <summary>
@@ -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<RepeatableQuestHelper> _logger)
public class RepeatableQuestHelper(
ISptLogger<RepeatableQuestHelper> _logger,
ConfigServer _configServer
)
{
protected QuestConfig _questConfig = _configServer.GetConfig<QuestConfig>();
/// <summary>
/// Get the relevant elimination config based on the current players PMC level
/// </summary>
@@ -22,4 +29,22 @@ public class RepeatableQuestHelper(ISptLogger<RepeatableQuestHelper> _logger)
pmcLevel >= x.LevelRange.Min && pmcLevel <= x.LevelRange.Max
);
}
/// <summary>
/// Returns the repeatable template ids for the provided side
/// </summary>
/// <param name="playerGroup">Side to get the templates for</param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public Dictionary<string, string>? 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)
};
}
}
@@ -0,0 +1,10 @@
using SPTarkov.Server.Core.Utils.Json.Converters;
namespace SPTarkov.Server.Core.Models.Enums;
[EftEnumConverter]
public enum PlayerGroup
{
Pmc,
Scav
}
@@ -15,73 +15,81 @@ public record QuestConfig : BaseConfig
/// Hours to get/redeem items from quest mail keyed by profile type
/// </summary>
[JsonPropertyName("mailRedeemTimeHours")]
public Dictionary<string, double?>? MailRedeemTimeHours { get; set; }
[JsonPropertyName("questTemplateIds")]
public PlayerTypeQuestIds? QuestTemplateIds { get; set; }
public required Dictionary<string, double> MailRedeemTimeHours { get; set; }
/// <summary>
/// Show non-seasonal quests be shown to player
/// Collection of quests by id only available to usec
/// </summary>
[JsonPropertyName("showNonSeasonalEventQuests")]
public bool? ShowNonSeasonalEventQuests { get; set; }
[JsonPropertyName("eventQuests")]
public Dictionary<string, EventQuestData>? EventQuests { get; set; }
[JsonPropertyName("repeatableQuests")]
public List<RepeatableQuestConfig>? RepeatableQuests { get; set; }
[JsonPropertyName("locationIdMap")]
public Dictionary<string, string>? LocationIdMap { get; set; }
[JsonPropertyName("bearOnlyQuests")]
public HashSet<string>? BearOnlyQuests { get; set; }
[JsonPropertyName("usecOnlyQuests")]
public HashSet<string>? UsecOnlyQuests { get; set; }
public required HashSet<string> UsecOnlyQuests { get; set; }
/// <summary>
/// Collection of quests by id only available to bears
/// </summary>
[JsonPropertyName("bearOnlyQuests")]
public required HashSet<string> BearOnlyQuests { get; set; }
/// <summary>
/// Quests that the keyed game version do not see/access
/// </summary>
[JsonPropertyName("profileBlacklist")]
public Dictionary<string, HashSet<string>>? ProfileBlacklist { get; set; }
public required Dictionary<string, HashSet<string>> ProfileBlacklist { get; set; }
/// <summary>
/// key=questid, gameversions that can see/access quest
/// </summary>
[JsonPropertyName("profileWhitelist")]
public Dictionary<string, HashSet<string>>? ProfileWhitelist { get; set; }
public required Dictionary<string, HashSet<string>> ProfileWhitelist { get; set; }
/// <summary>
/// Holds repeatable quest template ids for pmc's and scav's
/// </summary>
[JsonPropertyName("repeatableQuestTemplateIds")]
public required RepeatableQuestTemplates RepeatableQuestTemplates { get; set; }
/// <summary>
/// Show non-seasonal quests be shown to players
/// </summary>
[JsonPropertyName("showNonSeasonalEventQuests")]
public required bool ShowNonSeasonalEventQuests { get; set; }
/// <summary>
/// Collection of event quest data keyed by quest id.
/// </summary>
[JsonPropertyName("eventQuests")]
public required Dictionary<string, EventQuestData> EventQuests { get; set; }
/// <summary>
/// List of repeatable quest configs for; daily, weekly, and daily scav.
/// </summary>
[JsonPropertyName("repeatableQuests")]
public required List<RepeatableQuestConfig> RepeatableQuests { get; set; }
/// <summary>
/// Maps internal map names to their mongoId: Key - internal :: val - Mongoid
/// </summary>
[JsonPropertyName("locationIdMap")]
public required Dictionary<string, string> LocationIdMap { get; set; }
}
public record PlayerTypeQuestIds
public record RepeatableQuestTemplates
{
[JsonExtensionData]
public Dictionary<string, object> ExtensionData { get; set; }
/// <summary>
/// Pmc repeatable quest template ids keyed by type of quest
/// Keys: elimination, completion, exploration
/// </summary>
[JsonPropertyName("pmc")]
public QuestTypeIds? Pmc { get; set; }
public required Dictionary<string, string> Pmc { get; set; }
/// <summary>
/// Scav repeatable quest template ids keyed by type of quest
/// Keys: elimination, completion, exploration, pickup
/// </summary>
[JsonPropertyName("scav")]
public QuestTypeIds? Scav { get; set; }
}
public record QuestTypeIds
{
[JsonExtensionData]
public Dictionary<string, object> 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<string, string> Scav { get; set; }
}
public record EventQuestData
@@ -89,21 +97,36 @@ public record EventQuestData
[JsonExtensionData]
public Dictionary<string, object> ExtensionData { get; set; }
/// <summary>
/// Name of the event quest
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; set; }
public required string Name { get; set; }
/// <summary>
/// Season to which this quest belongs
/// </summary>
[JsonPropertyName("season")]
public SeasonalEventType? Season { get; set; }
public required SeasonalEventType Season { get; set; }
/// <summary>
/// Start timestamp
/// </summary>
[JsonPropertyName("startTimestamp")]
public long? StartTimestamp { get; set; }
public required long StartTimestamp { get; set; }
/// <summary>
/// End timestamp
/// </summary>
[JsonPropertyName("endTimestamp")]
[JsonConverter(typeof(StringToNumberFactoryConverter))]
public long? EndTimestamp { get; set; }
public required long EndTimestamp { get; set; }
/// <summary>
/// Is this quest part of a yearly event, ex: Christmas
/// </summary>
[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<string, object> ExtensionData { get; set; }
/// <summary>
/// Id for type of repeatable quest
/// </summary>
[JsonPropertyName("id")]
public string? Id { get; set; }
public required string Id { get; set; }
/// <summary>
/// Human-readable name for repeatable quest type
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; set; }
public required string Name { get; set; }
/// <summary>
/// Side this config belongs to. Note: Random not implemented, do not use!
/// </summary>
[JsonPropertyName("side")]
public string? Side { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public required PlayerGroup Side { get; set; }
/// <summary>
/// Types of tasks this config can generate; ex: Elimination
/// </summary>
[JsonPropertyName("types")]
public List<string>? Types { get; set; }
public required List<string> Types { get; set; }
/// <summary>
/// How long does the task stay active for after accepting it
/// </summary>
[JsonPropertyName("resetTime")]
public long? ResetTime { get; set; }
public required long ResetTime { get; set; }
/// <summary>
/// How many quests should we provide per ResetTime
/// </summary>
[JsonPropertyName("numQuests")]
public int? NumQuests { get; set; }
public required int NumQuests { get; set; }
/// <summary>
/// Min player level required to receive a quest from this config
/// </summary>
[JsonPropertyName("minPlayerLevel")]
public int? MinPlayerLevel { get; set; }
public required int MinPlayerLevel { get; set; }
/// <summary>
/// Reward scaling config
/// </summary>
[JsonPropertyName("rewardScaling")]
public RewardScaling? RewardScaling { get; set; }
public required RewardScaling RewardScaling { get; set; }
/// <summary>
/// Location map
/// </summary>
[JsonPropertyName("locations")]
public Dictionary<ELocationName, List<string>>? Locations { get; set; }
public required Dictionary<ELocationName, List<string>> Locations { get; set; }
/// <summary>
/// Traders that are allowed to generate tasks from this config.
/// Includes quest types, reward whitelist, and whether rewards can be weapons.
/// </summary>
[JsonPropertyName("traderWhitelist")]
public List<TraderWhitelist>? TraderWhitelist { get; set; }
public required List<TraderWhitelist> TraderWhitelist { get; set; }
/// <summary>
/// Quest config, holds information on how a task should be generated
/// </summary>
[JsonPropertyName("questConfig")]
public RepeatableQuestTypesConfig? QuestConfig { get; set; }
public RepeatableQuestTypesConfig QuestConfig { get; set; }
/// <summary>
/// Item base types to block when generating rewards
/// </summary>
[JsonPropertyName("rewardBaseTypeBlacklist")]
public HashSet<string>? RewardBaseTypeBlacklist { get; set; }
public required HashSet<string> RewardBaseTypeBlacklist { get; set; }
/// <summary>
/// Item tplIds to ignore when generating rewards
/// </summary>
[JsonPropertyName("rewardBlacklist")]
public HashSet<string>? RewardBlacklist { get; set; }
public required HashSet<string> RewardBlacklist { get; set; }
/// <summary>
/// Minimum stack size that an ammo reward should be generated with
/// </summary>
[JsonPropertyName("rewardAmmoStackMinSize")]
public int? RewardAmmoStackMinSize { get; set; }
public required int RewardAmmoStackMinSize { get; set; }
/// <summary>
/// How many free task changes are available from this config
/// </summary>
[JsonPropertyName("freeChangesAvailable")]
public int? FreeChangesAvailable { get; set; }
public required int FreeChangesAvailable { get; set; }
/// <summary>
/// How many free task changes remain from this config
/// </summary>
[JsonPropertyName("freeChanges")]
public int? FreeChanges { get; set; }
public required int FreeChanges { get; set; }
/// <summary>
/// Should the task replacement category be the same as the one its replacing
/// </summary>
[JsonPropertyName("keepDailyQuestTypeOnReplacement")]
public bool? KeepDailyQuestTypeOnReplacement { get; set; }
public required bool KeepDailyQuestTypeOnReplacement { get; set; }
/// <summary>
/// Reputation standing price for replacing a repeatable
/// </summary>
[JsonPropertyName("standingChangeCost")]
public IList<double>? StandingChangeCost { get; set; }
public required IList<double> StandingChangeCost { get; set; }
}
public record RewardScaling