cbcd431f09
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
424 lines
17 KiB
TypeScript
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}`);
|
|
}
|
|
|
|
} |