using System.Runtime.InteropServices.JavaScript; using SptCommon.Annotations; using Core.Helpers; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.ItemEvent; using Core.Models.Eft.Quests; using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Utils; using Core.Routers; using Core.Servers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; namespace Core.Controllers; [Injectable] public class QuestController( ISptLogger _logger, TimeUtil _timeUtil, HttpResponseUtil _httpResponseUtil, EventOutputHolder _eventOutputHolder, DatabaseService _databaseService, ItemHelper _itemHelper, DialogueHelper _dialogueHelper, MailSendService _mailSendService, ProfileHelper _profileHelper, TraderHelper _traderHelper, QuestHelper _questHelper, QuestRewardHelper _questRewardHelper, QuestConditionHelper _questConditionHelper, PlayerService _playerService, LocaleService _localeService, LocalisationService _localisationService, ConfigServer _configServer, ICloner _cloner ) { protected QuestConfig _questConfig = _configServer.GetConfig(); protected List _questTypes = ["PickUp", "Exploration", "Elimination"]; public List GetClientQuest(string sessionId) { return _questHelper.GetClientQuests(sessionId); } public ItemEventRouterResponse AcceptQuest(PmcData pmcData, AcceptQuestRequestData acceptedQuest, string sessionID) { var acceptQuestResponse = _eventOutputHolder.GetOutput(sessionID); // Does quest exist in profile // Restarting a failed quest can mean quest exists in profile var existingQuestStatus = pmcData.Quests.FirstOrDefault((x) => x.QId == acceptedQuest.QuestId); if (existingQuestStatus is not null) { // Update existing _questHelper.ResetQuestState(pmcData, QuestStatusEnum.Started, acceptedQuest.QuestId); // Need to send client an empty list of completedConditions (Unsure if this does anything) acceptQuestResponse.ProfileChanges[sessionID].QuestsStatus.Add(existingQuestStatus); } else { // Add new quest to server profile var newQuest = _questHelper.GetQuestReadyForProfile(pmcData, QuestStatusEnum.Started, acceptedQuest); pmcData.Quests.Add(newQuest); } // Create a dialog message for starting the quest. // Note that for starting quests, the correct locale field is "description", not "startedMessageText". var questFromDb = _questHelper.GetQuestFromDb(acceptedQuest.QuestId, pmcData); AddTaskConditionCountersToProfile(questFromDb.Conditions.AvailableForFinish, pmcData, acceptedQuest.QuestId); // Get messageId of text to send to player as text message in game var messageId = _questHelper.GetMessageIdForQuestStart( questFromDb.StartedMessageText, questFromDb.Description ); // Apply non-item rewards to profile + return item rewards var startedQuestRewardItems = _questRewardHelper.ApplyQuestReward( pmcData, acceptedQuest.QuestId, QuestStatusEnum.Started, sessionID, acceptQuestResponse ); // Send started text + any starting reward items found above to player _mailSendService.SendLocalisedNpcMessageToPlayer( sessionID, _traderHelper.GetTraderById(questFromDb.TraderId).ToString(), MessageType.QUEST_START, messageId, startedQuestRewardItems.ToList(), _timeUtil.GetHoursAsSeconds((int)_questHelper.GetMailItemRedeemTimeHoursForProfile(pmcData)) ); // Having accepted new quest, look for newly unlocked quests and inform client of them var newlyAccessibleQuests = _questHelper.GetNewlyAccessibleQuestsWhenStartingQuest( acceptedQuest.QuestId, sessionID ); if (newlyAccessibleQuests.Count > 0) { acceptQuestResponse.ProfileChanges[sessionID].Quests.AddRange(newlyAccessibleQuests); } return acceptQuestResponse; } private void AddTaskConditionCountersToProfile(List? questConditions, PmcData pmcData, string questId) { foreach (var condition in questConditions) { if (pmcData.TaskConditionCounters.TryGetValue(condition.Id, out var counter)) { _logger.Error( $"Unable to add new task condition counter: {condition.ConditionType} for qeust: {questId} to profile: {pmcData.SessionId} as it already exists:" ); } switch (condition.ConditionType) { case "SellItemToTrader": pmcData.TaskConditionCounters[condition.Id] = new TaskConditionCounter { Id = condition.Id, SourceId = questId, Type = condition.ConditionType, Value = 0, }; break; } } } public ItemEventRouterResponse AcceptRepeatableQuest(PmcData pmcData, AcceptQuestRequestData acceptedQuest, string sessionID) { // Create and store quest status object inside player profile var newRepeatableQuest = _questHelper.GetQuestReadyForProfile( pmcData, QuestStatusEnum.Started, acceptedQuest ); pmcData.Quests.Add(newRepeatableQuest); // Look for the generated quest cache in profile.RepeatableQuests var repeatableQuestProfile = GetRepeatableQuestFromProfile(pmcData, acceptedQuest); if (repeatableQuestProfile is null) { _logger.Error( _localisationService.GetText( "repeatable-accepted_repeatable_quest_not_found_in_active_quests", acceptedQuest.QuestId ) ); throw new Exception(_localisationService.GetText("repeatable-unable_to_accept_quest_see_log")); } // Some scav quests need to be added to scav profile for them to show up in-raid if (repeatableQuestProfile.Side == "Scav" && _questTypes.Contains(repeatableQuestProfile.Type.ToString())) { var fullProfile = _profileHelper.GetFullProfile(sessionID); if (fullProfile.CharacterData.ScavData.Quests is null) { fullProfile.CharacterData.ScavData.Quests = []; } fullProfile.CharacterData.ScavData.Quests.Add(newRepeatableQuest); } var response = _eventOutputHolder.GetOutput(sessionID); return response; } private RepeatableQuest GetRepeatableQuestFromProfile(PmcData pmcData, AcceptQuestRequestData acceptedQuest) { foreach (var repeatableQuest in pmcData.RepeatableQuests) { var matchingQuest = repeatableQuest.ActiveQuests.FirstOrDefault(x => x.Id == acceptedQuest.QuestId); if (matchingQuest is not null) { _logger.Debug($"Accepted repeatable quest {acceptedQuest.QuestId} from {repeatableQuest.Name}"); matchingQuest.SptRepatableGroupName = repeatableQuest.Name; return matchingQuest; } } return null; } public ItemEventRouterResponse CompleteQuest(PmcData pmcData, CompleteQuestRequestData info, string sessionId) { return _questHelper.CompleteQuest(pmcData, info, sessionId); } public ItemEventRouterResponse HandoverQuest(PmcData pmcData, HandoverQuestRequestData handoverQuestRequest, string sessionID) { var quest = _questHelper.GetQuestFromDb(handoverQuestRequest.QuestId, pmcData); List handoverQuestTypes = ["HandoverItem", "WeaponAssembly"]; var output = _eventOutputHolder.GetOutput(sessionID); var isItemHandoverQuest = true; var handedInCount = 0; // Decrement number of items handed in QuestCondition? handoverRequirements = null; foreach (var condition in quest.Conditions.AvailableForFinish) { if (condition.Id == handoverQuestRequest.ConditionId && handoverQuestTypes.Contains(condition.ConditionType)) { handedInCount = int.Parse((string)condition.Value); isItemHandoverQuest = condition.ConditionType == handoverQuestTypes.FirstOrDefault(); handoverRequirements = condition; if (pmcData.TaskConditionCounters.TryGetValue("ConditionId", out var counter)) { handedInCount -= (int)(counter.Value ?? 0); if (handedInCount <= 0) { _logger.Error( _localisationService.GetText( "repeatable-quest_handover_failed_condition_already_satisfied", new { questId = handoverQuestRequest.QuestId, conditionId = handoverQuestRequest.ConditionId, profileCounter = counter.Value, value = handedInCount } ) ); return output; } break; } } } if (isItemHandoverQuest && handedInCount == 0) { return ShowRepeatableQuestInvalidConditionError(handoverQuestRequest, output); } var totalItemCountToRemove = 0d; foreach (var itemHandover in handoverQuestRequest.Items) { var matchingItemInProfile = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == itemHandover.Id); if (!(matchingItemInProfile is not null && handoverRequirements.Target.List.Contains(matchingItemInProfile.Template))) { // Item handed in by player doesnt match what was requested return ShowQuestItemHandoverMatchError( handoverQuestRequest, matchingItemInProfile, handoverRequirements, output ); } // Remove the right quantity of given items var itemCountToRemove = Math.Min(itemHandover.Count ?? 0, handedInCount - totalItemCountToRemove); totalItemCountToRemove += itemCountToRemove; if (itemHandover.Count - itemCountToRemove > 0) { // Remove single item with no children _questHelper.ChangeItemStack( pmcData, itemHandover.Id, (int)(itemHandover.Count - itemCountToRemove ?? 0), sessionID, output ); if (totalItemCountToRemove == handedInCount) { break; } } else { // Remove item with children var toRemove = _itemHelper.FindAndReturnChildrenByItems(pmcData.Inventory.Items, itemHandover.Id); var index = pmcData.Inventory.Items.Count; // Important: don't tell the client to remove the attachments, it will handle it output.ProfileChanges[sessionID] .Items.DeletedItems.Add( new Item { Id = itemHandover.Id } ); // Important: loop backward when removing items from the array we're looping on while (index-- > 0) { if (toRemove.Contains(pmcData.Inventory.Items[index].Id)) { var removedItem = _cloner.Clone(pmcData.Inventory.Items[index]); pmcData.Inventory.Items.RemoveAt(index); // Remove the item // If the removed item has a numeric `location` property, re-calculate all the child // element `location` properties of the parent so they are sequential, while retaining order if (removedItem.Location.GetType() == typeof(int)) { var childItems = _itemHelper.FindAndReturnChildrenAsItems( pmcData.Inventory.Items, removedItem.ParentId ); childItems.RemoveAt(0); // Remove the parent // Sort by the current `location` and update childItems.Sort((a, b) => (((int)a.Location) > ((int)b.Location) ? 1 : -1)); for (int i = 0; i < childItems.Count; i++) { childItems[i].Location = i; } } } } } } UpdateProfileTaskConditionCounterValue( pmcData, handoverQuestRequest.ConditionId, handoverQuestRequest.QuestId, totalItemCountToRemove ); return output; } private ItemEventRouterResponse ShowRepeatableQuestInvalidConditionError(HandoverQuestRequestData handoverQuestRequest, ItemEventRouterResponse output) { var errorMessage = _localisationService.GetText( "repeatable-quest_handover_failed_condition_invalid", new { questId = handoverQuestRequest.QuestId, conditionId = handoverQuestRequest.ConditionId } ); _logger.Error(errorMessage); return _httpResponseUtil.AppendErrorToOutput(output, errorMessage); } private ItemEventRouterResponse ShowQuestItemHandoverMatchError(HandoverQuestRequestData handoverQuestRequest, Item? itemHandedOver, QuestCondition? handoverRequirements, ItemEventRouterResponse output) { var errorMessage = _localisationService.GetText( "quest-handover_wrong_item", new { questId = handoverQuestRequest.QuestId, handedInTpl = itemHandedOver?.Template ?? "UNKNOWN", requiredTpl = handoverRequirements.Target.List.FirstOrDefault(), } ); _logger.Error(errorMessage); return _httpResponseUtil.AppendErrorToOutput(output, errorMessage); } private void UpdateProfileTaskConditionCounterValue(PmcData pmcData, string conditionId, string questId, double counterValue) { if (pmcData.TaskConditionCounters[conditionId] != null) { pmcData.TaskConditionCounters[conditionId].Value += counterValue; return; } pmcData.TaskConditionCounters[conditionId] = new TaskConditionCounter { Id = conditionId, SourceId = questId, Type = "HandoverItem", Value = counterValue, }; } public ItemEventRouterResponse FailQuest(PmcData pmcData, FailQuestRequestData request, string sessionID, ItemEventRouterResponse output) { _questHelper.FailQuest(pmcData, request, sessionID, output); return output; } }