diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json index 2c3d87c9..feb67e91 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json @@ -62,6 +62,12 @@ "chat-unable_to_register_command_already_registered": "Unable to register already registered command: %s", "client_request": "[Client Request] %s", "client_request_ip": "[Client Request] {{ip}} {{url}}", + "custom-quest-service_quest_id_already_exists": "A quest with the id: {{questId}} already exists.", + "custom-quest-service_no_languages_for_quest": "No languages have been added for custom quest id: {{questId}}", + "custom-quest-service_no_entries_for_language": "No locale entries have been added for language key: {{languageKey}}, was this intentional?", + "custom-quest-service_could_not_find_language_key": "Could not find language key: {{languageKey}} in global locales when adding a custom quest. This is either a typo, or this language is not supported.", + "custom-quest-service_locale_data_null": "Locale data is null for language: {{languageKey}}", + "custom-quest-service_invalid_side": "QuestId: {{questId}} Savage is not a valid side for a side locked quest.", "customisation-item_already_purchased": "Clothing item {{itemId}} {{itemName}} already purchased", "customisation-suit_lacks_upd_or_stack_property": "Suit with tpl: %s lacks a upd object or stackobjectcount property", "customisation-unable_to_find_clothing_item_in_inventory": "Clothing item not found in inventory with id: %s", diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/NewQuestDetails.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/NewQuestDetails.cs new file mode 100644 index 00000000..e5ee3806 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/NewQuestDetails.cs @@ -0,0 +1,49 @@ +using System.Text.Json.Serialization; +using SPTarkov.Server.Core.Models.Common; +using SPTarkov.Server.Core.Models.Eft.Common.Tables; +using SPTarkov.Server.Core.Models.Enums; + +namespace SPTarkov.Server.Core.Models.Spt.Mod; + +/// +/// New quest detail object for use with the CustomQuestService. +/// +public record NewQuestDetails +{ + /// + /// Quest to be added to the database + /// + [JsonPropertyName("newQuest")] + public required Quest NewQuest { get; init; } + + /// + /// Locales for this quest. The primary key is the language to add to locale entries to
+ /// The secondary key is the locale key, the value is the locale text itself. + ///
+ [JsonPropertyName("locales")] + public required Dictionary> Locales { get; init; } + + /// + /// Only Usec and Bear are valid entries here, + /// if used it will lock that quest to only being available to that specific side.

+ /// + /// If not used, this should be left null to keep the quest open to both Usec and Bears. + ///
+ [JsonPropertyName("lockedToSide")] + public PlayerSide? LockedToSide { get; init; } +} + +/// +/// Result from either creating a new quest or cloning one. +/// +public record CreateQuestResult(bool Success, MongoId? QuestId) +{ + [JsonPropertyName("success")] + public bool Success { get; set; } = Success; + + [JsonPropertyName("questId")] + public MongoId? QuestId { get; set; } = QuestId; + + [JsonPropertyName("errors")] + public List Errors { get; } = []; +} diff --git a/Libraries/SPTarkov.Server.Core/Services/Mod/CustomQuestService.cs b/Libraries/SPTarkov.Server.Core/Services/Mod/CustomQuestService.cs new file mode 100644 index 00000000..230b96c9 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Services/Mod/CustomQuestService.cs @@ -0,0 +1,121 @@ +using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Models.Common; +using SPTarkov.Server.Core.Models.Enums; +using SPTarkov.Server.Core.Models.Spt.Config; +using SPTarkov.Server.Core.Models.Spt.Mod; +using SPTarkov.Server.Core.Servers; + +namespace SPTarkov.Server.Core.Services.Mod; + +[Injectable] +public class CustomQuestService( + DatabaseService databaseService, + ConfigServer configServer, + ServerLocalisationService serverLocalisationService +) +{ + /// + /// Create a new custom quest from a NewQuestDetails object. + /// + /// Quest details to be used for creation + /// Result of the quest creation, remember to check it for errors! + public CreateQuestResult CreateQuest(NewQuestDetails newQuestDetails) + { + var quest = newQuestDetails.NewQuest; + var result = new CreateQuestResult(false, newQuestDetails.NewQuest.Id); + + var databaseQuests = databaseService.GetTables().Templates.Quests; + if (!databaseQuests.TryAdd(quest.Id, quest)) + { + result.Errors.Add(serverLocalisationService.GetText("custom-quest-service_quest_id_already_exists", quest.Id)); + return result; + } + + var locales = newQuestDetails.Locales; + if (locales.Count == 0) + { + result.Errors.Add(serverLocalisationService.GetText("custom-quest-service_no_languages_for_quest", quest.Id)); + return result; + } + + AddQuestLocales(locales, result); + + var side = newQuestDetails.LockedToSide; + if (side.HasValue) + { + RestrictQuestSide(quest.Id, side.Value, result); + } + + // No errors mean success + result.Success = result.Errors.Count == 0; + return result; + } + + /// + /// Adds quest locales to the database + /// + /// locales to add + /// create quest result + private void AddQuestLocales(Dictionary> locales, CreateQuestResult result) + { + var globalLocales = databaseService.GetLocales().Global; + + foreach (var (languageKey, entries) in locales) + { + if (entries.Count == 0) + { + result.Errors?.Add(serverLocalisationService.GetText("custom-quest-service_no_entries_for_language", languageKey)); + continue; + } + + if (!globalLocales.TryGetValue(languageKey, out var lazyLoadedLocales)) + { + result.Errors?.Add(serverLocalisationService.GetText("custom-quest-service_could_not_find_language_key", languageKey)); + continue; + } + + lazyLoadedLocales.AddTransformer(localeData => + { + if (localeData is null) + { + result.Errors?.Add(serverLocalisationService.GetText("custom-quest-service_locale_data_null", languageKey)); + return null; + } + + foreach (var (key, entry) in entries) + { + localeData.TryAdd(key, entry); + } + + return localeData; + }); + } + } + + /// + /// Restricts a custom quest to a specific side. + /// + /// Quest id to restrict + /// Side to restrict it to + /// Result of the quest creation + private void RestrictQuestSide(MongoId questId, PlayerSide side, CreateQuestResult result) + { + var questConfig = configServer.GetConfig(); + + // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault + switch (side) + { + case PlayerSide.Usec: + questConfig.UsecOnlyQuests.Add(questId); + break; + + case PlayerSide.Bear: + questConfig.BearOnlyQuests.Add(questId); + break; + + case PlayerSide.Savage: + result.Errors.Add(serverLocalisationService.GetText("custom-quest-service_invalid_side", result.QuestId)); + break; + } + } +}