Server/project/src/services/MailSendService.ts
Dev cbcd431f09 Fixed new profiles failing to start game during christmas/new years event
Add new function in dialog helper for getting dialogs frm profile - creates empty object if none found

Set gift collection time to 48 hours if none supplied
2023-08-03 12:25:09 +01:00

424 lines
17 KiB
TypeScript

import { inject, injectable } from "tsyringe";
import { DialogueHelper } from "../helpers/DialogueHelper";
import { ItemHelper } from "../helpers/ItemHelper";
import { NotificationSendHelper } from "../helpers/NotificationSendHelper";
import { NotifierHelper } from "../helpers/NotifierHelper";
import { Item } from "../models/eft/common/tables/IItem";
import { Dialogue, IUserDialogInfo, Message, MessageItems } from "../models/eft/profile/IAkiProfile";
import { MessageType } from "../models/enums/MessageType";
import { Traders } from "../models/enums/Traders";
import { ISendMessageDetails } from "../models/spt/dialog/ISendMessageDetails";
import { ILogger } from "../models/spt/utils/ILogger";
import { DatabaseServer } from "../servers/DatabaseServer";
import { SaveServer } from "../servers/SaveServer";
import { HashUtil } from "../utils/HashUtil";
import { TimeUtil } from "../utils/TimeUtil";
import { LocalisationService } from "./LocalisationService";
@injectable()
export class MailSendService
{
protected readonly systemSenderId = "59e7125688a45068a6249071";
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("NotifierHelper") protected notifierHelper: NotifierHelper,
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper,
@inject("NotificationSendHelper") protected notificationSendHelper: NotificationSendHelper,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ItemHelper") protected itemHelper: ItemHelper
)
{ }
/**
* 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 playerId Players id to send message to
* @param sender 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 sendDirectNpcMessageToPlayer(playerId: string, sender: Traders, messageType: MessageType, message: string, items: Item[] = [], maxStorageTimeSeconds = null): void
{
if (!sender)
{
this.logger.error(`Unable to send message type: ${messageType} to player: ${playerId}, provided trader enum was null`);
return;
}
const details: ISendMessageDetails = {
recipientId: playerId,
sender: messageType,
dialogType: MessageType.NPC_TRADER,
trader: sender,
messageText: message
};
// Add items to message
if (items.length > 0)
{
details.items = items;
details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds;
}
this.sendMessageToPlayer(details);
}
/**
* Send a message from an NPC (e.g. prapor) to the player with or without items
* @param playerId Players id to send message to
* @param sender 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 sendLocalisedNpcMessageToPlayer(playerId: string, sender: Traders, messageType: MessageType, messageLocaleId: string, items: Item[] = [], maxStorageTimeSeconds = null): void
{
if (!sender)
{
this.logger.error(`Unable to send message type: ${messageType} to player: ${playerId}, provided trader enum was null`);
return;
}
const details: ISendMessageDetails = {
recipientId: playerId,
sender: messageType,
dialogType: MessageType.NPC_TRADER,
trader: sender,
templateId: messageLocaleId
};
// Add items to message
if (items.length > 0)
{
details.items = items;
details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds;
}
this.sendMessageToPlayer(details);
}
/**
* Send a message from SYSTEM to the player with or without items
* @param playerId Players id to send 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 sendSystemMessageToPlayer(playerId: string, message: string, items: Item[] = [], maxStorageTimeSeconds = null): void
{
const details: ISendMessageDetails = {
recipientId: playerId,
sender: MessageType.SYSTEM_MESSAGE,
messageText: message
};
// Add items to message
if (items.length > 0)
{
details.items = items;
details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds;
}
this.sendMessageToPlayer(details);
}
/**
* Send a USER message to a player with or without items
* @param playerId Players id to send 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 sendUserMessageToPlayer(playerId: string, senderDetails: IUserDialogInfo, message: string, items: Item[] = [], maxStorageTimeSeconds = null): void
{
const details: ISendMessageDetails = {
recipientId: playerId,
sender: MessageType.USER_MESSAGE,
senderDetails: senderDetails,
messageText: message
};
// Add items to message
if (items.length > 0)
{
details.items = items;
details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds;
}
this.sendMessageToPlayer(details);
}
/**
* Large function to send messages to players from a variety of sources (SYSTEM/NPC/USER)
* Helper functions in this class are availble to simplify common actions
* @param messageDetails Details needed to send a message to the player
*/
public sendMessageToPlayer(messageDetails: ISendMessageDetails): void
{
// Get dialog, create if doesn't exist
const senderDialog = this.getDialog(messageDetails);
// Flag dialog as containing a new message to player
senderDialog.new++;
// Craft message
const message = this.createDialogMessage(senderDialog._id, messageDetails);
// Create items array
// Generate item stash if we have rewards.
const itemsToSendToPlayer = this.processItemsBeforeAddingToMail(senderDialog.type, messageDetails);
// If there's items to send to player, flag dialog as containing attachments
if (itemsToSendToPlayer.data?.length > 0)
{
senderDialog.attachmentsNew += 1;
}
// Store reward items inside message and set appropriate flags inside message
this.addRewardItemsToMessage(message, itemsToSendToPlayer, messageDetails.itemsMaxStorageLifetimeSeconds);
// Add message to dialog
senderDialog.messages.push(message);
// TODO: clean up old code here
// Offer Sold notifications are now separate from the main notification
if (senderDialog.type === MessageType.FLEAMARKET_MESSAGE && messageDetails.ragfairDetails)
{
const offerSoldMessage = this.notifierHelper.createRagfairOfferSoldNotification(message, messageDetails.ragfairDetails);
this.notificationSendHelper.sendMessage(messageDetails.recipientId, offerSoldMessage);
message.type = MessageType.MESSAGE_WITH_ITEMS; // Should prevent getting the same notification popup twice
}
// Send message off to player so they get it in client
const notificationMessage = this.notifierHelper.createNewMessageNotification(message);
this.notificationSendHelper.sendMessage(messageDetails.recipientId, notificationMessage);
}
/**
* Send a message from the player to an NPC
* @param sessionId Player id
* @param targetNpcId NPC message is sent to
* @param message Text to send to NPC
*/
public sendPlayerMessageToNpc(sessionId: string, targetNpcId: string, message: string): void
{
const playerProfile = this.saveServer.getProfile(sessionId);
const dialogWithNpc = playerProfile.dialogues[targetNpcId];
if (!dialogWithNpc)
{
this.logger.error(`Dialog for: ${targetNpcId} does not exist`);
}
dialogWithNpc.messages.push({
_id: sessionId, // players id
dt: this.timeUtil.getTimestamp(),
hasRewards: false,
items: {},
uid: playerProfile.characters.pmc._id,
type: MessageType.USER_MESSAGE,
rewardCollected: false,
text: message
});
}
/**
* Create a message for storage inside a dialog in the player profile
* @param senderDialog Id of dialog that will hold the message
* @param messageDetails Various details on what the message must contain/do
* @returns Message
*/
protected createDialogMessage(dialogId: string, messageDetails: ISendMessageDetails): Message
{
const message: Message = {
_id: this.hashUtil.generate(),
uid: dialogId, // must match the dialog id
type: messageDetails.sender, // Same enum is used for defining dialog type + message type, thanks bsg
dt: Math.round(Date.now() / 1000),
text: messageDetails.templateId ? "" : messageDetails.messageText, // store empty string if template id has value, otherwise store raw message text
templateId: messageDetails.templateId, // used by traders to send localised text from database\locales\global
hasRewards: false, // The default dialog message has no rewards, can be added later via addRewardItemsToMessage()
rewardCollected: false, // The default dialog message has no rewards, can be added later via addRewardItemsToMessage()
systemData: messageDetails.systemData ? messageDetails.systemData : undefined, // Used by ragfair
profileChangeEvents: (messageDetails.profileChangeEvents?.length === 0) ? messageDetails.profileChangeEvents : undefined // no one knows, its never been used in any dumps
};
// Clean up empty system data
if (!message.systemData)
{
delete message.systemData;
}
// Clean up empty template id
if (!message.templateId)
{
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
*/
protected addRewardItemsToMessage(message: Message, itemsToSendToPlayer: MessageItems, maxStorageTimeSeconds: number): void
{
if (itemsToSendToPlayer?.data?.length > 0)
{
message.items = itemsToSendToPlayer;
message.hasRewards = true;
message.maxStorageTime = maxStorageTimeSeconds;
message.rewardCollected = false;
}
}
/**
* perform various sanitising actions on the items before they're considered ready for insertion into message
* @param dialogType The type of the dialog that will hold the reward items being processed
* @param messageDetails
* @returns Sanitised items
*/
protected processItemsBeforeAddingToMail(dialogType: MessageType, messageDetails: ISendMessageDetails): MessageItems
{
const db = this.databaseServer.getTables().templates.items;
let itemsToSendToPlayer: MessageItems = {};
if (messageDetails.items?.length > 0)
{
// No parent id, generate random id and add (doesnt need to be actual parentId from db, only unique)
if (!messageDetails.items[0]?.parentId)
{
messageDetails.items[0].parentId = this.hashUtil.generate();
}
itemsToSendToPlayer = {
stash: messageDetails.items[0].parentId,
data: []
};
// Ensure Ids are unique and cont collide with items in player invenory later
messageDetails.items = this.itemHelper.replaceIDs(null, messageDetails.items);
for (const reward of messageDetails.items)
{
// Ensure item exists in items db
const itemTemplate = db[reward._tpl];
if (!itemTemplate)
{
// Can happen when modded items are insured + mod is removed
this.logger.error(this.localisationService.getText("dialog-missing_item_template", {tpl: reward._tpl, type: dialogType}));
continue;
}
// Ensure every 'base/root' item has the same parentId + has a slotid of 'main'
if (!("slotId" in reward) || reward.slotId === "hideout")
{
// Reward items NEED a parent id + slotid
reward.parentId = messageDetails.items[0].parentId;
reward.slotId = "main";
}
// Item is sanitised and ready to be put into holding array
itemsToSendToPlayer.data.push(reward);
// Item can contain sub-items, add those to array e.g. ammo boxes
if ("StackSlots" in itemTemplate._props)
{
const stackSlotItems = this.itemHelper.generateItemsFromStackSlot(itemTemplate, reward._id);
for (const itemToAdd of stackSlotItems)
{
itemsToSendToPlayer.data.push(itemToAdd);
}
}
}
// Remove empty data property if no rewards
if (itemsToSendToPlayer.data.length === 0)
{
delete itemsToSendToPlayer.data;
}
}
return itemsToSendToPlayer;
}
/**
* 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
*/
protected getDialog(messageDetails: ISendMessageDetails): Dialogue
{
const dialogsInProfile = this.dialogueHelper.getDialogsForProfile(messageDetails.recipientId);
const senderId = this.getMessageSenderIdByType(messageDetails);
// Does dialog exist
let senderDialog = dialogsInProfile[senderId];
if (!senderDialog)
{
// Create if doesnt
dialogsInProfile[senderId] = {
_id: senderId,
type: messageDetails.dialogType ? messageDetails.dialogType : messageDetails.sender,
messages: [],
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
*/
protected getMessageSenderIdByType(messageDetails: ISendMessageDetails): string
{
if (messageDetails.sender === MessageType.SYSTEM_MESSAGE)
{
return this.systemSenderId;
}
if (messageDetails.sender === MessageType.NPC_TRADER || messageDetails.dialogType === MessageType.NPC_TRADER)
{
return Traders[messageDetails.trader];
}
if (messageDetails.sender === MessageType.USER_MESSAGE)
{
return messageDetails.senderDetails?._id;
}
if (messageDetails.senderDetails?._id)
{
return messageDetails.senderDetails._id;
}
if (messageDetails.trader)
{
return Traders[messageDetails.trader];
}
this.logger.warning(`Unable to handle message of type: ${messageDetails.sender}`);
}
}