From 7f411c808edf779f96ae67562689351b1ae62b7a Mon Sep 17 00:00:00 2001 From: CWX Date: Tue, 14 Jan 2025 16:50:17 +0000 Subject: [PATCH] implement services --- Core/Services/GiftService.cs | 152 ++++++++- Core/Services/MailSendService.cs | 560 +++++++++++++++++++++++++++++++ 2 files changed, 700 insertions(+), 12 deletions(-) create mode 100644 Core/Services/MailSendService.cs diff --git a/Core/Services/GiftService.cs b/Core/Services/GiftService.cs index c5c7b952..e2648a4e 100644 --- a/Core/Services/GiftService.cs +++ b/Core/Services/GiftService.cs @@ -2,7 +2,9 @@ using Core.Annotations; using Core.Helpers; using Core.Models.Enums; using Core.Models.Spt.Config; +using Core.Models.Spt.Dialog; using Core.Servers; +using Core.Utils; using ILogger = Core.Models.Utils.ILogger; namespace Core.Services; @@ -11,18 +13,34 @@ namespace Core.Services; public class GiftService { private readonly ILogger _logger; - private readonly ConfigServer _configServer; + + private readonly MailSendService _mailSendService; + private readonly LocalisationService _localisationService; + private readonly HashUtil _hashUtil; + private readonly TimeUtil _timeUtil; private readonly ProfileHelper _profileHelper; + private readonly ConfigServer _configServer; + private readonly GiftsConfig _giftConfig; - public GiftService( + public GiftService + ( ILogger logger, - ConfigServer configServer, - ProfileHelper profileHelper) + MailSendService mailSendService, + LocalisationService localisationService, + HashUtil hashUtil, + TimeUtil timeUtil, + ProfileHelper profileHelper, + ConfigServer configServer + ) { _logger = logger; - _configServer = configServer; + _mailSendService = mailSendService; + _localisationService = localisationService; + _hashUtil = hashUtil; + _timeUtil = timeUtil; _profileHelper = profileHelper; + _configServer = configServer; _giftConfig = _configServer.GetConfig(ConfigTypes.GIFTS); } @@ -34,7 +52,7 @@ public class GiftService */ public bool GiftExists(string giftId) { - throw new NotImplementedException(); + return _giftConfig.Gifts[giftId] is not null; } public Gift GetGiftById(string giftId) @@ -50,7 +68,7 @@ public class GiftService */ public Dictionary GetGifts() { - throw new NotImplementedException(); + return _giftConfig.Gifts; } /** @@ -59,7 +77,7 @@ public class GiftService */ public List GetGiftIds() { - throw new NotImplementedException(); + return _giftConfig.Gifts.Keys.ToList(); } /** @@ -90,7 +108,96 @@ public class GiftService _logger.Warning($"Gift {giftId} has items but no collection time limit, defaulting to 48 hours"); } - throw new NotImplementedException(); + // Handle system messsages + if (giftData.Sender == GiftSenderType.System) + { + // Has a localisable text id to send to player + if (giftData.LocaleTextId is not null) + { + _mailSendService.SendLocalisedSystemMessageToPlayer( + playerId, + giftData.LocaleTextId, + giftData.Items, + giftData.ProfileChangeEvents, + _timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 1)); + } + else + { + _mailSendService.SendSystemMessageToPlayer( + playerId, + giftData.MessageText, + giftData.Items, + _timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 1), + giftData.ProfileChangeEvents); + } + } + // Handle user messages + else if (giftData.Sender == GiftSenderType.User) + { + _mailSendService.SendUserMessageToPlayer( + playerId, + giftData.SenderDetails, + giftData.MessageText, + giftData.Items, + _timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 1)); + } + else if (giftData.Sender == GiftSenderType.Trader) + { + if (giftData.LocaleTextId is not null) + { + _mailSendService.SendLocalisedNpcMessageToPlayer( + playerId, + giftData.Trader, + MessageType.MESSAGE_WITH_ITEMS, + giftData.LocaleTextId, + giftData.Items, + _timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 1), + null, + null + ); + } + else + { + _mailSendService.SendLocalisedNpcMessageToPlayer( + playerId, + giftData.Trader, + MessageType.MESSAGE_WITH_ITEMS, + giftData.MessageText, + giftData.Items, + _timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 1), + null, + null + ); + } + } + else + { + // TODO: further split out into different message systems like above SYSTEM method + // Trader / ragfair + SendMessageDetails details = new () { + RecipientId = playerId, + Sender = GetMessageType(giftData), + SenderDetails = new () + { + Id = GetSenderId(giftData), + Aid = 1234567, // TODO - pass proper aid value + Info = null, + }, + MessageText = giftData.MessageText, + Items = giftData.Items, + ItemsMaxStorageLifetimeSeconds = _timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 0), + }; + + if (giftData.Trader is not null) { + details.Trader = giftData.Trader; + } + + _mailSendService.SendMessageToPlayer(details); + } + + _profileHelper.FlagGiftReceivedInProfile(playerId, giftId, maxGiftsToSendCount); + + return GiftSentResult.SUCCESS; } /** @@ -98,9 +205,19 @@ public class GiftService * @param giftData Gift to send player * @returns trader/user/system id */ - protected string? GetSenderId(Gift giftData) + private string? GetSenderId(Gift giftData) { - throw new NotImplementedException(); + if (giftData.Sender == GiftSenderType.Trader) + { + return Enum.GetName(typeof(GiftSenderType), giftData.Sender); + } + + if (giftData.Sender == GiftSenderType.User) + { + return giftData.Sender.ToString(); + } + + return null; } /** @@ -110,7 +227,18 @@ public class GiftService */ protected MessageType? GetMessageType(Gift giftData) { - throw new NotImplementedException(); + switch (giftData.Sender) + { + case GiftSenderType.System: + return MessageType.SYSTEM_MESSAGE; + case GiftSenderType.Trader: + return MessageType.NPC_TRADER; + case GiftSenderType.User: + return MessageType.USER_MESSAGE; + default: + _logger.Error(_localisationService.GetText("gift-unable_to_handle_message_type_command", giftData.Sender)); + return null; + } } /** diff --git a/Core/Services/MailSendService.cs b/Core/Services/MailSendService.cs new file mode 100644 index 00000000..242efb62 --- /dev/null +++ b/Core/Services/MailSendService.cs @@ -0,0 +1,560 @@ +using Core.Annotations; +using Core.Helpers; +using Core.Models.Eft.Common.Tables; +using Core.Models.Eft.Profile; +using Core.Models.Enums; +using Core.Models.Spt.Dialog; +using Core.Servers; +using Core.Utils; +using ILogger = Core.Models.Utils.ILogger; + +namespace Core.Services; + +[Injectable] +public class MailSendService +{ + private readonly ILogger _logger; + private readonly HashUtil _hashUtil; + private readonly TimeUtil _timeUtil; + private readonly SaveServer _saveServer; + private readonly DatabaseService _databaseService; + private readonly NotifierHelper _notifierHelper; + private readonly DialogueHelper _dialogueHelper; + private readonly NotificationSendHelper _notificationSendHelper; + private readonly LocalisationService _localisationService; + private readonly ItemHelper _itemHelper; + private readonly TraderHelper _traderHelper; + + private const string _systemSenderId = "59e7125688a45068a6249071"; + private readonly List _messageTypes = [MessageType.NPC_TRADER, MessageType.FLEAMARKET_MESSAGE]; + private readonly List _slotNames = ["hideout", "main"]; + + public MailSendService + ( + ILogger logger, + HashUtil hashUtil, + TimeUtil timeUtil, + SaveServer saveServer, + DatabaseService databaseService, + NotifierHelper notifierHelper, + DialogueHelper dialogueHelper, + NotificationSendHelper notificationSendHelper, + LocalisationService localisationService, + ItemHelper itemHelper, + TraderHelper traderHelper + ) + { + _logger = logger; + _hashUtil = hashUtil; + _timeUtil = timeUtil; + _saveServer = saveServer; + _databaseService = databaseService; + _notifierHelper = notifierHelper; + _dialogueHelper = dialogueHelper; + _localisationService = localisationService; + _itemHelper = itemHelper; + _traderHelper = traderHelper; + } + + /** + * Send a message from an NPC (e.g. prapor) to the player with or without items using direct message text, do not look up any locale + * @param sessionId The session ID to send the message to + * @param trader The trader sending the message + * @param messageType What type the message will assume (e.g. QUEST_SUCCESS) + * @param message Text to send to the player + * @param items Optional items to send to player + * @param maxStorageTimeSeconds Optional time to collect items before they expire + */ + public void SendDirectNpcMessageToPlayer( + string sessionId, + string trader, + MessageType messageType, + string message, + List? items, + long? maxStorageTimeSeconds, + SystemData? systemData, + MessageContentRagfair? ragfair + ) + { + if (trader is null) + { + _logger.Error(_localisationService.GetText("mailsend-missing_trader", new + { + MessageType = messageType, + SessionId = sessionId, + })); + + return; + } + + SendMessageDetails details = new() + { + RecipientId = sessionId, + Sender = messageType, + DialogType = MessageType.NPC_TRADER, + Trader = trader, + MessageText = message, + }; + + // Add items to message + if (items?.Count > 0) + { + details.Items.AddRange(items); + details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; + } + + if (systemData is not null) + details.SystemData = systemData; + + if (ragfair is not null) + details.RagfairDetails = ragfair; + + SendMessageToPlayer(details); + } + + /** + * Send a message from an NPC (e.g. prapor) to the player with or without items + * @param sessionId The session ID to send the message to + * @param trader The trader sending the message + * @param messageType What type the message will assume (e.g. QUEST_SUCCESS) + * @param messageLocaleId The localised text to send to player + * @param items Optional items to send to player + * @param maxStorageTimeSeconds Optional time to collect items before they expire + */ + public void SendLocalisedNpcMessageToPlayer( + string sessionId, + string trader, + MessageType messageType, + string messageLocaleId, + List? items, + long? maxStorageTimeSeconds, + SystemData? systemData, + MessageContentRagfair? ragfair + ) + { + if (trader is null) + { + _logger.Error(_localisationService.GetText("mailsend-missing_trader", new + { + MessageType = messageType, + SessionId = sessionId, + })); + + return; + } + + SendMessageDetails details = new() + { + RecipientId = sessionId, + Sender = messageType, + DialogType = MessageType.NPC_TRADER, + Trader = trader, + TemplateId = messageLocaleId, + }; + + // add items to message + + if (items?.Count > 0) + { + details.Items.AddRange(items); + details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; + } + + if (systemData is not null) + details.SystemData = systemData; + + if (ragfair is not null) + details.RagfairDetails = ragfair; + + SendMessageToPlayer(details); + } + + /** + * Send a message from SYSTEM to the player with or without items + * @param sessionId The session ID to send the message to + * @param message The text to send to player + * @param items Optional items to send to player + * @param maxStorageTimeSeconds Optional time to collect items before they expire + */ + public void SendSystemMessageToPlayer( + string sessionId, + string message, + List? items, + long? maxStorageTimeSeconds, + List? profileChangeEvents) + { + SendMessageDetails details = new() + { + RecipientId = sessionId, + Sender = MessageType.SYSTEM_MESSAGE, + MessageText = message, + }; + + // add items to message + if (items?.Count > 0) + { + var rootItemParentId = _hashUtil.Generate(); + + details.Items.AddRange(_itemHelper.AdoptOrphanedItems(rootItemParentId, items)); + details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; + } + + if ((profileChangeEvents?.Count ?? 0) > 0) + details.ProfileChangeEvents = profileChangeEvents; + + SendMessageToPlayer(details); + } + + public void SendLocalisedSystemMessageToPlayer( + string sessionId, + string messageLocaleId, + List? items, + List? profileChangeEvents, + long? maxStorageTimeSeconds + ) + { + SendMessageDetails details = new() + { + RecipientId = sessionId, + Sender = MessageType.SYSTEM_MESSAGE, + TemplateId = messageLocaleId + }; + + // add items to message + if (items?.Count > 0) + { + details.Items.AddRange(items); + details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; + } + + if ((profileChangeEvents?.Count ?? 0) > 0) + details.ProfileChangeEvents = profileChangeEvents; + + SendMessageToPlayer(details); + } + + /** + * Send a USER message to a player with or without items + * @param sessionId The session ID to send the message to + * @param senderId Who is sending the message + * @param message The text to send to player + * @param items Optional items to send to player + * @param maxStorageTimeSeconds Optional time to collect items before they expire + */ + public void SendUserMessageToPlayer( + string sessionId, + UserDialogInfo senderDetails, + string message, + List? items, + long? maxStorageTimeSeconds + ) + { + SendMessageDetails details = new() + { + RecipientId = sessionId, + Sender = MessageType.USER_MESSAGE, + SenderDetails = senderDetails, + MessageText = message + }; + + // add items to message + if (items?.Count > 0) + { + details.Items.AddRange(items); + details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; + } + + SendMessageToPlayer(details); + } + + /** + * Large function to send messages to players from a variety of sources (SYSTEM/NPC/USER) + * Helper functions in this class are available to simplify common actions + * @param messageDetails Details needed to send a message to the player + */ + public void SendMessageToPlayer(SendMessageDetails messageDetails) + { + // Get dialog, create if doesn't exist + var senderDialog = GetDialog(messageDetails); + + // Flag dialog as containing a new message to player + senderDialog.New++; + + // Craft message + var message = CreateDialogMessage(senderDialog.Id, messageDetails); + + // Create items array + // Generate item stash if we have rewards. + var itemsToSendToPlayer = ProcessItemsBeforeAddingToMail(senderDialog.Type, messageDetails); + + // If there's items to send to player, flag dialog as containing attachments + if ((itemsToSendToPlayer.Data?.Count ?? 0) > 0) + { + senderDialog.AttachmentsNew += 1; + } + + // Store reward items inside message and set appropriate flags inside message + AddRewardItemsToMessage(message, itemsToSendToPlayer, messageDetails.ItemsMaxStorageLifetimeSeconds); + + if (messageDetails.ProfileChangeEvents is not null) + message.ProfileChangeEvents = messageDetails.ProfileChangeEvents; + + // Add message to dialog + senderDialog.Messages.Add(message); + + // TODO: clean up old code here + // Offer Sold notifications are now separate from the main notification + if ( + _messageTypes.Contains(senderDialog.Type ?? MessageType.SYSTEM_MESSAGE) && + messageDetails?.RagfairDetails is not null + ) + { + var offerSoldMessage = _notifierHelper.CreateRagfairOfferSoldNotification( + message, + messageDetails.RagfairDetails + ); + _notificationSendHelper.SendMessage(messageDetails.RecipientId, offerSoldMessage); + message.MessageType = MessageType.MESSAGE_WITH_ITEMS; // Should prevent getting the same notification popup twice + } + + // Send message off to player so they get it in client + var notificationMessage = _notifierHelper.CreateNewMessageNotification(message); + _notificationSendHelper.SendMessage(messageDetails.RecipientId, notificationMessage); + } + + public void SendPlayerMessageToNpc(string sessionId, string targetNpcId, string message) + { + var playerProfile = _saveServer.GetProfile(sessionId); + var dialogWithNpc = playerProfile.DialogueRecords[targetNpcId]; + if (dialogWithNpc is null) + { + _logger.Error(_localisationService.GetText("mailsend-missing_npc_dialog", targetNpcId)); + return; + } + + dialogWithNpc.Messages.Add(new() + { + Id = _hashUtil.Generate(), + DateTime = _timeUtil.GetTimeStamp(), + HasRewards = false, + UserId = playerProfile.CharacterData.PmcData.Id, + MessageType = MessageType.USER_MESSAGE, + RewardCollected = false, + Text = message + }); + } + + private Message CreateDialogMessage(string dialogId, SendMessageDetails messageDetails) + { + Message message = new() + { + Id = _hashUtil.Generate(), + UserId = dialogId, + MessageType = messageDetails.DialogType, + DateTime = _timeUtil.GetTimeStamp(), + Text = (messageDetails.TemplateId is not null) ? "" : messageDetails.MessageText, + TemplateId = messageDetails.TemplateId, + HasRewards = false, + RewardCollected = false, + SystemData = messageDetails.SystemData is not null ? messageDetails.SystemData : null, + ProfileChangeEvents = (messageDetails.ProfileChangeEvents?.Count == 0) ? messageDetails.ProfileChangeEvents : null + }; + + // Clean up empty system data + // if (message.SystemData is null) { + // delete message.SystemData; + // } + + // Clean up empty template id + // if (message.TemplateId is null) + // delete message.templateId; + + return message; + } + + /** + * Add items to message and adjust various properties to reflect the items being added + * @param message Message to add items to + * @param itemsToSendToPlayer Items to add to message + * @param maxStorageTimeSeconds total time items are stored in mail before being deleted + */ + private void AddRewardItemsToMessage(Message message, MessageItems? itemsToSendToPlayer, long? maxStorageTimeSeconds) + { + if ((itemsToSendToPlayer?.Data?.Count ?? 0) > 0) + { + message.Items = itemsToSendToPlayer; + message.HasRewards = true; + message.MaxStorageTime = maxStorageTimeSeconds; + message.RewardCollected = false; + } + } + + private MessageItems ProcessItemsBeforeAddingToMail(MessageType? dialogType, SendMessageDetails messageDetails) + { + var items = _databaseService.GetItems(); + + MessageItems itemsToSendToPlayer = new(); + if ((messageDetails.Items?.Count ?? 0) > 0) + { + // Find base item that should be the 'primary' + have its parent id be used as the dialogs 'stash' value + var parentItem = GetBaseItemFromRewards(messageDetails.Items); + if (parentItem is null) + { + _localisationService.GetText("mailsend-missing_parent", new + { + TraderId = messageDetails.Trader, + Sender = messageDetails.Sender, + }); + + return itemsToSendToPlayer; + } + + // No parent id, generate random id and add (doesn't need to be actual parentId from db, only unique) + if (parentItem?.ParentId is null) + parentItem.ParentId = _hashUtil.Generate(); + + itemsToSendToPlayer = new() + { + Stash = parentItem.ParentId, Data = new() + }; + + // Ensure Ids are unique and cont collide with items in player inventory later + messageDetails.Items = _itemHelper.ReplaceIDs(messageDetails.Items); + + foreach (var reward in messageDetails.Items) + { + // Ensure item exists in items db + var itemTemplate = items[reward.Template]; + if (itemTemplate is null) + { + _logger.Error(_localisationService.GetText("dialog-missing_item_template", new + { + Tpl = reward.Template, + Type = dialogType, + })); + + continue; + } + + // Ensure every 'base/root' item has the same parentId + has a slotid of 'main' + if (!(reward.SlotId is not null) || reward.SlotId == "hideout" || reward.ParentId == parentItem.ParentId) + { + // Reward items NEED a parent id + slotid + reward.ParentId = parentItem.ParentId; + reward.SlotId = "main"; + } + + // Boxes can contain sub-items + if (_itemHelper.IsOfBaseclass(itemTemplate.Id, BaseClasses.AMMO_BOX)) + { + var boxAndCartridges = new List(); + boxAndCartridges.Add(reward); + _itemHelper.AddCartridgesToAmmoBox(boxAndCartridges, itemTemplate); + + // Push box + cartridge children into array + itemsToSendToPlayer.Data.AddRange(boxAndCartridges); + } + else + { + if (itemTemplate.Properties.StackSlots is not null) + _logger.Error(_localisationService.GetText("mail-unable_to_give_gift_not_handled", itemTemplate.Id)); + + // Item is sanitised and ready to be pushed into holding array + itemsToSendToPlayer.Data.Add(reward); + } + } + + // Remove empty data property if no rewards + // if (itemsToSendToPlayer.Data.Count == 0) + // delete itemsToSendToPlayer.data; + } + + return itemsToSendToPlayer; + } + + /** + * Try to find the most correct item to be the 'primary' item in a reward mail + * @param items Possible items to choose from + * @returns Chosen 'primary' item + */ + private Item GetBaseItemFromRewards(List? items) + { + // Only one item in reward, return it + if (items?.Count == 1) + return items[0]; + + // Find first item with slotId that indicates its a 'base' item + var item = items.FirstOrDefault(x => _slotNames.Contains(x.SlotId ?? "")); + if (item is not null) + return item; + + // Not a singlular item + no items have a hideout/main slotid + // Look for first item without parent id + item = items.FirstOrDefault(x => x.ParentId is null); + if (item is not null) + return item; + + // Just return first item in array + return items[0]; + } + + /** + * Get a dialog with a specified entity (user/trader) + * Create and store empty dialog if none exists in profile + * @param messageDetails Data on what message should do + * @returns Relevant Dialogue + */ + private Dialogue GetDialog(SendMessageDetails messageDetails) + { + var dialogsInProfile = _dialogueHelper.GetDialogsForProfile(messageDetails.RecipientId); + var senderId = GetMessageSenderIdByType(messageDetails); + if (senderId is null) + throw new Exception(_localisationService.GetText("mail-unable_to_find_message_sender_by_id", messageDetails.Sender)); + + // Does dialog exist + var senderDialog = dialogsInProfile[senderId]; + if (senderDialog is null) + { + // create if doesnt + dialogsInProfile[senderId] = senderDialog = new Dialogue() + { + Id = senderId, + Type = (messageDetails.DialogType is not null) ? messageDetails.DialogType : messageDetails.Sender, + Messages = new(), + Pinned = false, + New = 0, + AttachmentsNew = 0 + }; + + senderDialog = dialogsInProfile[senderId]; + } + + return senderDialog; + } + + /** + * Get the appropriate sender id by the sender enum type + * @param messageDetails + * @returns gets an id of the individual sending it + */ + private string? GetMessageSenderIdByType(SendMessageDetails messageDetails) + { + if (messageDetails.Sender == MessageType.SYSTEM_MESSAGE) + return _systemSenderId; + + if (messageDetails.Sender == MessageType.NPC_TRADER || messageDetails.DialogType == MessageType.NPC_TRADER) + return (messageDetails.Trader is not null) ? _traderHelper.GetValidTraderIdByEnumValue(messageDetails.Trader) : null; + + if (messageDetails.Sender == MessageType.USER_MESSAGE) + return messageDetails.SenderDetails?.Id; + + if (messageDetails.SenderDetails?.Id is not null) + return messageDetails.SenderDetails.Id; + + if (messageDetails.Trader is not null) + return _traderHelper.GetValidTraderIdByEnumValue(messageDetails.Trader); + + _logger.Warning($"Unable to handle message of type: {messageDetails.Sender}"); + return null; + } +}