Rework message sending to support gift system (!106)

Co-authored-by: Kaeno <e>
Co-authored-by: Dev <dev@dev.sp-tarkov.com>
Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/106
This commit is contained in:
chomp 2023-07-21 17:08:32 +00:00
parent a9e6d5d0fe
commit a7b4ebe316
22 changed files with 1361 additions and 78 deletions

View File

@ -0,0 +1,570 @@
{
"gifts": {
"NewYear2021": {
"sender": "System",
"messageText": "<ch>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</ch><cz>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</cz><en>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</en><fr>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</fr><ge>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</ge><hu>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</hu><it>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</it><jp>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</jp><kr>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</kr><pl>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</pl><po>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</po><sk>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</sk><es>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</es><es-mx>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</es-mx><tu>Hello there, friend. Happy New Year to you! You've probably forgotten that holidays exist, but I think it's important to remain human in any situation. Wishing you lots of working graphics cards this year and that your Hideout never lets you down. Take this gift and be happy!</tu><ru>Ну, здравствуй, друг! С Новым годом тебя! Ты наверное забыл уже, что есть праздники, но я считаю, важно оставаться людьми в любой ситуации. Работающих видеокарт тебе в новом году и убежище чтобы не подводило. Держи и радуйся!</ru>",
"collectionTimeHours": 20,
"associatedEvent": "NewYears",
"items": [
{
"_id": "64b997574a8c8ed8cb019004",
"_tpl": "5c1a1e3f2e221602b66cc4c2",
"parentId": "62b997444agc8eb4cb013004",
"slotId": "main"
},
{
"_id": "64b397573a8c8ed8cb019002",
"_tpl": "5aafbde786f774389d0cbc0f",
"parentId": "62b997444agc8eb4cb013004",
"slotId": "main"
},
{
"_id": "64b397573a8c8ed8cb019007",
"_tpl": "58dd3ad986f77403051cba8f",
"parentId": "62b997444agc8eb4cb013004",
"slotId": "main",
"upd": {
"StackObjectsCount": 40
}
},
{
"_id": "64b397573a8c8ed8cb019008",
"_tpl": "58dd3ad986f77403051cba8f",
"parentId": "62b997444agc8eb4cb013004",
"slotId": "main",
"upd": {
"StackObjectsCount": 40
}
},
{
"_id": "64b397573a8c8ed8cb019009",
"_tpl": "58dd3ad986f77403051cba8f",
"parentId": "62b997444agc8eb4cb013004",
"slotId": "main",
"upd": {
"StackObjectsCount": 40
}
},
{
"_id": "64b497573b8c8ee8cb019009",
"_tpl": "619cbf9e0a7c3a1a2731940a",
"parentId": "62b997444agc8eb4cb013004",
"slotId": "main"
}
]
},
"Halloween 2023": {
"items": [
{
"_id": "1274650827982e40930946f4",
"_tpl": "6389c6463485cf0eeb260715",
"upd": {
"StackObjectsCount": 30
},
"parentId": "64b99751053fc8a45106fa55"
}
],
"sender": "System",
"messageText": "Halloween Event Text",
"timestampToSend": 42069,
"collectionTimeHours": 48,
"associatedEvent": "Halloween"
},
"Christmas 2022": {
"items": [
{
"_id": "a89075c1a18874dd6404a6d7",
"_tpl": "5aafbde786f774389d0cbc0f",
"upd": {
"StackObjectsCount": 5
}
}
],
"sender": "System",
"messageText": "Christmas Event Text",
"timestampToSend": 42069,
"associatedEvent": "Christmas"
},
"1CLICKDRESSUP": {
"items": [
{
"_id": "64b996c9d0de4697180359b7",
"_tpl": "5dcbd56fdbd3d91b3e5468d5",
"upd": {
"FireMode": {
"FireMode": "single"
}
},
"parentId": "64b996c9d0de4697180359b6",
"slotId": "main"
},
{
"_id": "64b996c9d0de4697180359b8",
"_tpl": "5c48a2c22e221602b313fb6c",
"parentId": "64b996c9d0de4697180359b7",
"slotId": "mod_pistol_grip"
},
{
"_id": "64b996c9d0de4697180359b9",
"_tpl": "5a3501acc4a282000d72293a",
"parentId": "64b996c9d0de4697180359b7",
"slotId": "mod_magazine"
},
{
"_id": "64b996c9d0de4697180359ba",
"_tpl": "5dcbd6b46ec07c0c4347a564",
"parentId": "64b996c9d0de4697180359b7",
"slotId": "mod_handguard"
},
{
"_id": "64b996c9d0de4697180359bb",
"_tpl": "5b7be4895acfc400170e2dd5",
"parentId": "64b996c9d0de4697180359ba",
"slotId": "mod_mount_000"
},
{
"_id": "64b996c9d0de4697180359bc",
"_tpl": "58c157be86f77403c74b2bb6",
"parentId": "64b996c9d0de4697180359bb",
"slotId": "mod_foregrip"
},
{
"_id": "64b996c9d0de4697180359bd",
"_tpl": "5c06595c0db834001a66af6c",
"parentId": "64b996c9d0de4697180359ba",
"slotId": "mod_tactical"
},
{
"_id": "64b996c9d0de4697180359be",
"_tpl": "5dcbe9431e1f4616d354987e",
"parentId": "64b996c9d0de4697180359b7",
"slotId": "mod_barrel"
},
{
"_id": "64b996c9d0de4697180359bf",
"_tpl": "5a34fd2bc4a282329a73b4c5",
"parentId": "64b996c9d0de4697180359be",
"slotId": "mod_muzzle"
},
{
"_id": "64b996c9d0de4697180359c0",
"_tpl": "59db7eed86f77461f8380365",
"parentId": "64b996c9d0de4697180359b7",
"slotId": "mod_scope"
},
{
"_id": "64b996c9d0de4697180359c1",
"_tpl": "5c052a900db834001a66acbd",
"parentId": "64b996c9d0de4697180359c0",
"slotId": "mod_scope"
},
{
"_id": "64b996c9d0de4697180359c2",
"_tpl": "5a33bab6c4a28200741e22f8",
"parentId": "64b996c9d0de4697180359c1",
"slotId": "mod_mount"
},
{
"_id": "64b996c9d0de4697180359c3",
"_tpl": "5a32aa8bc4a2826c6e06d737",
"parentId": "64b996c9d0de4697180359c2",
"slotId": "mod_scope"
},
{
"_id": "64b996c9d0de4697180359c4",
"_tpl": "5dfa3d7ac41b2312ea33362a",
"parentId": "64b996c9d0de4697180359b7",
"slotId": "mod_sight_rear"
},
{
"_id": "64b996c9d0de4697180359c5",
"_tpl": "628b9c7d45122232a872358f",
"parentId": "64b996c9d0de4697180359b6",
"slotId": "main"
},
{
"_id": "64b996c9d0de4697180359c6",
"_tpl": "62963c18dbc8ab5f0d382d0b",
"parentId": "64b996c9d0de4697180359b6",
"slotId": "main"
},
{
"_id": "64b996c9d0de4697180359c7",
"_tpl": "59fb023c86f7746d0d4b423c",
"parentId": "64b996c9d0de4697180359b6",
"slotId": "main"
}
],
"sender": "System",
"messageText": "1CLICKDRESSUP",
"timestampToSend": 42069,
"collectionTimeHours": 48,
"associatedEvent": "Promo"
},
"BARMALEY": {
"items": [
{
"_id": "64b99751053fc8a45106fa55",
"_tpl": "5cadf6eeae921500134b2799",
"upd": {
"StackObjectsCount": 30
},
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa56",
"_tpl": "5cadf6eeae921500134b2799",
"upd": {
"StackObjectsCount": 30
},
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa57",
"_tpl": "5cadf6eeae921500134b2799",
"upd": {
"StackObjectsCount": 30
},
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa58",
"_tpl": "5cadf6eeae921500134b2799",
"upd": {
"StackObjectsCount": 30
},
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa59",
"_tpl": "62330bfadc5883093563729b",
"upd": {
"StackObjectsCount": 30
},
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa5a",
"_tpl": "62330bfadc5883093563729b",
"upd": {
"StackObjectsCount": 30
},
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa5b",
"_tpl": "62330bfadc5883093563729b",
"upd": {
"StackObjectsCount": 30
},
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa5c",
"_tpl": "62330bfadc5883093563729b",
"upd": {
"StackObjectsCount": 30
},
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa5d",
"_tpl": "5d1b376e86f774252519444e",
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa5e",
"_tpl": "5aafbde786f774389d0cbc0f",
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa5f",
"_tpl": "5aafbde786f774389d0cbc0f",
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa60",
"_tpl": "633ec7c2a6918cb895019c6c",
"upd": {
"Repairable": {
"MaxDurability": 100,
"Durability": 100
},
"FireMode": {
"FireMode": "single"
}
},
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa61",
"_tpl": "633ec6ee025b096d320a3b15",
"parentId": "64b99751053fc8a45106fa60",
"slotId": "mod_magazine"
},
{
"_id": "64b99751053fc8a45106fa62",
"_tpl": "633ec8e4025b096d320a3b1e",
"parentId": "64b99751053fc8a45106fa60",
"slotId": "mod_pistol_grip"
},
{
"_id": "64b99751053fc8a45106fa63",
"_tpl": "61a4c8884f95bc3b2c5dc96f",
"upd": {
"FireMode": {
"FireMode": "single"
}
},
"parentId": "64b99751053fc8a45106fa54",
"slotId": "main"
},
{
"_id": "64b99751053fc8a45106fa64",
"_tpl": "619f54a1d25cbd424731fb99",
"parentId": "64b99751053fc8a45106fa63",
"slotId": "mod_magazine"
},
{
"_id": "64b99751053fc8a45106fa65",
"_tpl": "619f4cee4c58466fe1228435",
"parentId": "64b99751053fc8a45106fa63",
"slotId": "mod_sight_rear"
},
{
"_id": "64b99751053fc8a45106fa66",
"_tpl": "619f4d304c58466fe1228437",
"parentId": "64b99751053fc8a45106fa63",
"slotId": "mod_sight_front"
},
{
"_id": "64b99751053fc8a45106fa67",
"_tpl": "619f4bffd25cbd424731fb97",
"parentId": "64b99751053fc8a45106fa63",
"slotId": "mod_pistol_grip"
}
],
"sender": "System",
"messageText": "BARMALEY",
"timestampToSend": 42069,
"collectionTimeHours": 48,
"associatedEvent": "Promo"
},
"S00NS00N": {
"items": [
{
"_id": "64b997574a8c8eb8cb019005",
"_tpl": "619cbf9e0a7c3a1a2731940a",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
},
{
"_id": "64b997574a8c8eb8cb019006",
"_tpl": "5c94bbff86f7747ee735c08f",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
},
{
"_id": "64b997574a8c8eb8cb019007",
"_tpl": "5c94bbff86f7747ee735c08f",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
},
{
"_id": "64b997574a8c8eb8cb019008",
"_tpl": "5c94bbff86f7747ee735c08f",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
},
{
"_id": "64b997574a8c8eb8cb019009",
"_tpl": "5c94bbff86f7747ee735c08f",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
},
{
"_id": "64b997574a8c8eb8cb01900a",
"_tpl": "5c94bbff86f7747ee735c08f",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
},
{
"_id": "64b997574a8c8eb8cb01900b",
"_tpl": "5aafbcd986f7745e590fff23",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
},
{
"_id": "64b997574a8c8eb8cb01900c",
"_tpl": "5c0e530286f7747fa1419862",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
},
{
"_id": "64b997574a8c8eb8cb01900d",
"_tpl": "5c0e534186f7747fa1419867",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
},
{
"_id": "64b997574a8c8eb8cb01900e",
"_tpl": "5c0e533786f7747fa23f4d47",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
},
{
"_id": "64b997574a8c8eb8cb01900f",
"_tpl": "544fb3f34bdc2d03748b456a",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
},
{
"_id": "64b997574a8c8eb8cb019010",
"_tpl": "5c0e531d86f7747fa23f4d42",
"parentId": "64b997574a8c8eb8cb019004",
"slotId": "main"
}
],
"sender": "System",
"messageText": "S00NS00N",
"timestampToSend": 42069,
"collectionTimeHours": 48,
"associatedEvent": "Promo"
},
"TRAMBON": {
"items": [
{
"_id": "64b9975c0863630f260c592a",
"_tpl": "591094e086f7747caa7bb2ef",
"parentId": "64b9975c0863630f260c5929",
"slotId": "main"
},
{
"_id": "64b9975c0863630f260c592b",
"_tpl": "5e4abb5086f77406975c9342",
"parentId": "64b9975c0863630f260c5929",
"slotId": "main"
},
{
"_id": "64b9975c0863630f260c592c",
"_tpl": "5ca2151486f774244a3b8d30",
"parentId": "64b9975c0863630f260c5929",
"slotId": "main"
},
{
"_id": "64b9975c0863630f260c592d",
"_tpl": "60a3c68c37ea821725773ef5",
"parentId": "64b9975c0863630f260c5929",
"slotId": "main"
}
],
"sender": "System",
"messageText": "TRAMBON",
"timestampToSend": 42069,
"collectionTimeHours": 48,
"associatedEvent": "Promo"
},
"PINEWOOD": {
"items": [
{
"_id": "64b998310a0c2d62990eecc3",
"_tpl": "63a39fc0af870e651d58e6ae",
"parentId": "64b998310a0c2d62990eecc2",
"slotId": "main"
},
{
"_id": "64b998310a0c2d62990eecc4",
"_tpl": "5fc382a9d724d907e2077dab",
"upd": {
"StackObjectsCount": 20
},
"parentId": "64b998310a0c2d62990eecc2",
"slotId": "main"
},
{
"_id": "64b998310a0c2d62990eecc5",
"_tpl": "5fc22d7c187fea44d52eda44",
"upd": {
"FireMode": {
"FireMode": "single"
}
},
"parentId": "64b998310a0c2d62990eecc2",
"slotId": "main"
},
{
"_id": "64b998310a0c2d62990eecc6",
"_tpl": "57c55efc2459772d2c6271e7",
"parentId": "64b998310a0c2d62990eecc5",
"slotId": "mod_pistol_grip"
},
{
"_id": "64b998310a0c2d62990eecc7",
"_tpl": "5fc23426900b1d5091531e15",
"parentId": "64b998310a0c2d62990eecc5",
"slotId": "mod_magazine"
},
{
"_id": "64b998310a0c2d62990eecc8",
"_tpl": "5649be884bdc2d79388b4577",
"parentId": "64b998310a0c2d62990eecc5",
"slotId": "mod_stock_001"
},
{
"_id": "64b998310a0c2d62990eecc9",
"_tpl": "5fc2369685fd526b824a5713",
"parentId": "64b998310a0c2d62990eecc8",
"slotId": "mod_stock_000"
},
{
"_id": "64b998310a0c2d62990eecca",
"_tpl": "5fc278107283c4046c581489",
"parentId": "64b998310a0c2d62990eecc5",
"slotId": "mod_reciever"
},
{
"_id": "64b998310a0c2d62990eeccb",
"_tpl": "5fc23678ab884124df0cd590",
"parentId": "64b998310a0c2d62990eecca",
"slotId": "mod_barrel"
},
{
"_id": "64b998310a0c2d62990eeccc",
"_tpl": "5fc23636016cce60e8341b05",
"parentId": "64b998310a0c2d62990eeccb",
"slotId": "mod_muzzle"
},
{
"_id": "64b998310a0c2d62990eeccd",
"_tpl": "5fc2360f900b1d5091531e19",
"parentId": "64b998310a0c2d62990eeccb",
"slotId": "mod_gas_block"
},
{
"_id": "64b998310a0c2d62990eecce",
"_tpl": "5fc235db2770a0045c59c683",
"parentId": "64b998310a0c2d62990eecca",
"slotId": "mod_handguard"
}
],
"sender": "System",
"messageText": "PINEWOOD",
"timestampToSend": 42069,
"collectionTimeHours": 48,
"associatedEvent": "Promo"
}
}
}

View File

@ -1,5 +1,6 @@
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
import { GiftSentResult } from "@spt-aki/models/enums/GiftSentResult";
import { DialogueHelper } from "../helpers/DialogueHelper"; import { DialogueHelper } from "../helpers/DialogueHelper";
import { IGetAllAttachmentsResponse } from "../models/eft/dialog/IGetAllAttachmentsResponse"; import { IGetAllAttachmentsResponse } from "../models/eft/dialog/IGetAllAttachmentsResponse";
import { IGetFriendListDataResponse } from "../models/eft/dialog/IGetFriendListDataResponse"; import { IGetFriendListDataResponse } from "../models/eft/dialog/IGetFriendListDataResponse";
@ -11,7 +12,10 @@ import { ISendMessageRequest } from "../models/eft/dialog/ISendMessageRequest";
import { Dialogue, DialogueInfo, IAkiProfile, IUserDialogInfo, Message } from "../models/eft/profile/IAkiProfile"; import { Dialogue, DialogueInfo, IAkiProfile, IUserDialogInfo, Message } from "../models/eft/profile/IAkiProfile";
import { MemberCategory } from "../models/enums/MemberCategory"; import { MemberCategory } from "../models/enums/MemberCategory";
import { MessageType } from "../models/enums/MessageType"; import { MessageType } from "../models/enums/MessageType";
import { ILogger } from "../models/spt/utils/ILogger";
import { SaveServer } from "../servers/SaveServer"; import { SaveServer } from "../servers/SaveServer";
import { GiftService } from "../services/GiftService";
import { MailSendService } from "../services/MailSendService";
import { HashUtil } from "../utils/HashUtil"; import { HashUtil } from "../utils/HashUtil";
import { TimeUtil } from "../utils/TimeUtil"; import { TimeUtil } from "../utils/TimeUtil";
@ -19,12 +23,15 @@ import { TimeUtil } from "../utils/TimeUtil";
export class DialogueController export class DialogueController
{ {
constructor( constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("SaveServer") protected saveServer: SaveServer, @inject("SaveServer") protected saveServer: SaveServer,
@inject("TimeUtil") protected timeUtil: TimeUtil, @inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper, @inject("DialogueHelper") protected dialogueHelper: DialogueHelper,
@inject("MailSendService") protected mailSendService: MailSendService,
@inject("GiftService") protected giftService: GiftService,
@inject("HashUtil") protected hashUtil: HashUtil @inject("HashUtil") protected hashUtil: HashUtil
) )
{ } {}
/** Handle onUpdate spt event */ /** Handle onUpdate spt event */
public update(): void public update(): void
@ -46,7 +53,7 @@ export class DialogueController
return { return {
"Friends": [ "Friends": [
{ {
_id: "sptfriend", _id: "sptFriend",
Info: { Info: {
Level: 1, Level: 1,
MemberCategory: MemberCategory.DEVELOPER, MemberCategory: MemberCategory.DEVELOPER,
@ -89,25 +96,36 @@ export class DialogueController
const dialogue = this.saveServer.getProfile(sessionID).dialogues[dialogueID]; const dialogue = this.saveServer.getProfile(sessionID).dialogues[dialogueID];
const result: DialogueInfo = { const result: DialogueInfo = {
"_id": dialogueID, _id: dialogueID,
"type": dialogue.type ? dialogue.type : MessageType.NPC_TRADER, type: dialogue.type ? dialogue.type : MessageType.NPC_TRADER,
"message": this.dialogueHelper.getMessagePreview(dialogue), message: this.dialogueHelper.getMessagePreview(dialogue),
"new": dialogue.new, new: dialogue.new,
"attachmentsNew": dialogue.attachmentsNew, attachmentsNew: dialogue.attachmentsNew,
"pinned": dialogue.pinned, pinned: dialogue.pinned,
Users: this.getDialogueUsers(dialogue.Users, dialogue.type, sessionID) Users: this.getDialogueUsers(dialogue, dialogue.type, sessionID)
}; };
return result; return result;
} }
/**
public getDialogueUsers(users: IUserDialogInfo[], messageType: MessageType, sessionID: string): IUserDialogInfo[] * Todo
* @param users
* @param messageType
* @param sessionID
* @returns
*/
public getDialogueUsers(dialog: Dialogue, messageType: MessageType, sessionID: string): IUserDialogInfo[]
{ {
const profile = this.saveServer.getProfile(sessionID); const profile = this.saveServer.getProfile(sessionID);
if (messageType === MessageType.USER_MESSAGE && !users.find(x => x._id === profile.characters.pmc._id)) if (messageType === MessageType.USER_MESSAGE && !dialog.Users?.find(x => x._id === profile.characters.pmc._id))
{ {
users.push({ if (!dialog.Users)
{
dialog.Users = [];
}
dialog.Users.push({
_id: profile.characters.pmc._id, _id: profile.characters.pmc._id,
info: { info: {
Level: profile.characters.pmc.Info.Level, Level: profile.characters.pmc.Info.Level,
@ -118,7 +136,7 @@ export class DialogueController
}); });
} }
return users ? users : undefined; return dialog.Users ? dialog.Users : undefined;
} }
/** /**
@ -184,7 +202,12 @@ export class DialogueController
return profile.dialogues[request.dialogId]; return profile.dialogues[request.dialogId];
} }
/**
* TODO
* @param pmcProfile
* @param dialogUsers
* @returns
*/
protected getProfilesForMail(pmcProfile: IAkiProfile, dialogUsers: IUserDialogInfo[]): IUserDialogInfo[] protected getProfilesForMail(pmcProfile: IAkiProfile, dialogUsers: IUserDialogInfo[]): IUserDialogInfo[]
{ {
const result: IUserDialogInfo[] = []; const result: IUserDialogInfo[] = [];
@ -285,37 +308,51 @@ export class DialogueController
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
public sendMessage(sessionId: string, request: ISendMessageRequest): string public sendMessage(sessionId: string, request: ISendMessageRequest): string
{ {
const profile = this.saveServer.getProfile(sessionId); this.mailSendService.sendPlayerMessageToNpc(sessionId, request.dialogId, request.text);
const dialog = profile.dialogues[request.dialogId];
dialog.messages.push({
_id: sessionId,
dt: this.timeUtil.getTimestamp(),
hasRewards: false,
items: {},
uid: profile.characters.pmc._id,
type: MessageType.USER_MESSAGE,
rewardCollected: false,
text: request.text
});
if (request.dialogId.includes("sptfriend") && request.text.includes("love you")) // Handle when player types a keyword to sptfriend user
if (request.dialogId.includes("sptFriend"))
{ {
dialog.messages.push({ this.handleChatWithSPTFriend(sessionId, request);
_id: "sptfriend",
dt: this.timeUtil.getTimestamp()+1,
hasRewards: false,
items: {},
uid: "sptfriend",
type: MessageType.USER_MESSAGE,
rewardCollected: false,
text: "i love you too buddy :3"
});
dialog.new = 1;
} }
return request.dialogId; return request.dialogId;
} }
protected handleChatWithSPTFriend(sessionId: string, request: ISendMessageRequest): void
{
const sptFriendUser: IUserDialogInfo = {
_id: "sptFriend",
info: {
Level: 1,
MemberCategory: MemberCategory.DEVELOPER,
Nickname: "SPT",
Side: "Usec"
}
};
const giftSent = this.giftService.sendGiftToPlayer(sessionId, request.text);
if (giftSent === GiftSentResult.SUCCESS)
{
this.mailSendService.sendUserMessageToPlayer(sessionId, sptFriendUser, "hey! you got the right code!");
}
if (giftSent === GiftSentResult.FAILED_GIFT_ALREADY_RECEIVED)
{
this.mailSendService.sendUserMessageToPlayer(sessionId, sptFriendUser, "You already have that!!");
}
if (request.text.toLowerCase().includes("love you"))
{
this.mailSendService.sendUserMessageToPlayer(sessionId, sptFriendUser, "I love you too buddy :3!");
}
if (request.text.toLowerCase() === "spt")
{
this.mailSendService.sendUserMessageToPlayer(sessionId, sptFriendUser, "its me!!");
}
}
/** /**
* Get messages from a specific dialog that have items not expired * Get messages from a specific dialog that have items not expired
* @param sessionId Session id * @param sessionId Session id

View File

@ -584,7 +584,7 @@ export class GameController
if (!traderAssorts.loyal_level_items[assortKey]) if (!traderAssorts.loyal_level_items[assortKey])
{ {
// reverse lookup of enum key by value // reverse lookup of enum key by value
this.logger.warning(this.localisationService.getText("assort-missing_quest_assort_unlock", {traderName: Object.keys(Traders)[Object.values(Traders).indexOf(traderId)], questName: quests[questKey].QuestName})); this.logger.debug(this.localisationService.getText("assort-missing_quest_assort_unlock", {traderName: Object.keys(Traders)[Object.values(Traders).indexOf(traderId)], questName: quests[questKey].QuestName}));
} }
} }
} }

View File

@ -5,6 +5,7 @@ import { ItemHelper } from "../helpers/ItemHelper";
import { ProfileHelper } from "../helpers/ProfileHelper"; import { ProfileHelper } from "../helpers/ProfileHelper";
import { QuestConditionHelper } from "../helpers/QuestConditionHelper"; import { QuestConditionHelper } from "../helpers/QuestConditionHelper";
import { QuestHelper } from "../helpers/QuestHelper"; import { QuestHelper } from "../helpers/QuestHelper";
import { TraderHelper } from "../helpers/TraderHelper";
import { IPmcData } from "../models/eft/common/IPmcData"; import { IPmcData } from "../models/eft/common/IPmcData";
import { Quest } from "../models/eft/common/tables/IBotBase"; import { Quest } from "../models/eft/common/tables/IBotBase";
import { Item } from "../models/eft/common/tables/IItem"; import { Item } from "../models/eft/common/tables/IItem";
@ -26,6 +27,7 @@ import { ConfigServer } from "../servers/ConfigServer";
import { DatabaseServer } from "../servers/DatabaseServer"; import { DatabaseServer } from "../servers/DatabaseServer";
import { LocaleService } from "../services/LocaleService"; import { LocaleService } from "../services/LocaleService";
import { LocalisationService } from "../services/LocalisationService"; import { LocalisationService } from "../services/LocalisationService";
import { MailSendService } from "../services/MailSendService";
import { PlayerService } from "../services/PlayerService"; import { PlayerService } from "../services/PlayerService";
import { SeasonalEventService } from "../services/SeasonalEventService"; import { SeasonalEventService } from "../services/SeasonalEventService";
import { HttpResponseUtil } from "../utils/HttpResponseUtil"; import { HttpResponseUtil } from "../utils/HttpResponseUtil";
@ -44,7 +46,9 @@ export class QuestController
@inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper, @inject("DialogueHelper") protected dialogueHelper: DialogueHelper,
@inject("MailSendService") protected mailSendService: MailSendService,
@inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("TraderHelper") protected traderHelper: TraderHelper,
@inject("QuestHelper") protected questHelper: QuestHelper, @inject("QuestHelper") protected questHelper: QuestHelper,
@inject("QuestConditionHelper") protected questConditionHelper: QuestConditionHelper, @inject("QuestConditionHelper") protected questConditionHelper: QuestConditionHelper,
@inject("PlayerService") protected playerService: PlayerService, @inject("PlayerService") protected playerService: PlayerService,
@ -427,9 +431,14 @@ export class QuestController
protected sendSuccessDialogMessageOnQuestComplete(sessionID: string, pmcData: IPmcData, completedQuestId: string, questRewards: Reward[]): void protected sendSuccessDialogMessageOnQuestComplete(sessionID: string, pmcData: IPmcData, completedQuestId: string, questRewards: Reward[]): void
{ {
const quest = this.questHelper.getQuestFromDb(completedQuestId, pmcData); const quest = this.questHelper.getQuestFromDb(completedQuestId, pmcData);
const messageContent = this.dialogueHelper.createMessageContext(quest.successMessageText, MessageType.QUEST_SUCCESS, this.questConfig.redeemTime);
this.dialogueHelper.addDialogueMessage(quest.traderId, messageContent, sessionID, questRewards); this.mailSendService.sendLocalisedNpcMessageToPlayer(
sessionID,
this.traderHelper.getTraderById(quest.traderId),
MessageType.QUEST_SUCCESS,
quest.successMessageText,
questRewards,
this.timeUtil.getHoursAsSeconds(this.questConfig.redeemTime));
} }
/** /**

View File

@ -201,6 +201,7 @@ import { ItemBaseClassService } from "../services/ItemBaseClassService";
import { ItemFilterService } from "../services/ItemFilterService"; import { ItemFilterService } from "../services/ItemFilterService";
import { LocaleService } from "../services/LocaleService"; import { LocaleService } from "../services/LocaleService";
import { LocalisationService } from "../services/LocalisationService"; import { LocalisationService } from "../services/LocalisationService";
import { MailSendService } from "../services/MailSendService";
import { MatchBotDetailsCacheService } from "../services/MatchBotDetailsCacheService"; import { MatchBotDetailsCacheService } from "../services/MatchBotDetailsCacheService";
import { MatchLocationService } from "../services/MatchLocationService"; import { MatchLocationService } from "../services/MatchLocationService";
import { ModCompilerService } from "../services/ModCompilerService"; import { ModCompilerService } from "../services/ModCompilerService";
@ -610,6 +611,7 @@ export class Container
depContainer.register<TraderPurchasePersisterService>("TraderPurchasePersisterService", TraderPurchasePersisterService); depContainer.register<TraderPurchasePersisterService>("TraderPurchasePersisterService", TraderPurchasePersisterService);
depContainer.register<PmcChatResponseService>("PmcChatResponseService", PmcChatResponseService); depContainer.register<PmcChatResponseService>("PmcChatResponseService", PmcChatResponseService);
depContainer.register<GiftService>("GiftService", GiftService); depContainer.register<GiftService>("GiftService", GiftService);
depContainer.register<MailSendService>("MailSendService", MailSendService);
} }
private static registerServers(depContainer: DependencyContainer): void private static registerServers(depContainer: DependencyContainer): void

View File

@ -429,13 +429,13 @@ export class BotGenerator
if (botInfo.Nickname.toLowerCase() === "nikita") if (botInfo.Nickname.toLowerCase() === "nikita")
{ {
botInfo.GameVersion = "edge_of_darkness"; botInfo.GameVersion = "edge_of_darkness";
botInfo.AccountType = MemberCategory.DEVELOPER; botInfo.MemberCategory = MemberCategory.DEVELOPER;
return; return;
} }
botInfo.GameVersion = this.weightedRandomHelper.getWeightedValue(this.botConfig.pmc.gameVersionWeight); botInfo.GameVersion = this.weightedRandomHelper.getWeightedValue(this.botConfig.pmc.gameVersionWeight);
botInfo.AccountType = Number.parseInt(this.weightedRandomHelper.getWeightedValue(this.botConfig.pmc.accountTypeWeight)); botInfo.MemberCategory = Number.parseInt(this.weightedRandomHelper.getWeightedValue(this.botConfig.pmc.accountTypeWeight));
} }
/** /**

View File

@ -356,7 +356,7 @@ export class BotWeaponGenerator
this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot([EquipmentSlots.SECURED_CONTAINER], id, ammoTpl, [{ this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot([EquipmentSlots.SECURED_CONTAINER], id, ammoTpl, [{
_id: id, _id: id,
_tpl: ammoTpl, _tpl: ammoTpl,
upd: { "StackObjectsCount": stackSize } upd: { StackObjectsCount: stackSize }
}], }],
inventory); inventory);
} }

View File

@ -162,6 +162,7 @@ export class BotWeaponGeneratorHelper
if (!container) if (!container)
{ {
// Desired equipment container (e.g. backpack) not found // Desired equipment container (e.g. backpack) not found
this.logger.warning(`Unable to add items to bot slot: ${slot}, slot missing`);
continue; continue;
} }

View File

@ -29,27 +29,25 @@ export class DialogueHelper
{ } { }
/** /**
* Create basic message context template * @deprecated Use MailSendService.sendMessage() or helpers
* @param templateId
* @param messageType
* @param maxStoreTime
* @returns
*/ */
public createMessageContext(templateId: string, messageType: MessageType, maxStoreTime: number): MessageContent public createMessageContext(templateId: string, messageType: MessageType, maxStoreTime = null): MessageContent
{ {
return { const result: MessageContent = {
templateId: templateId, templateId: templateId,
type: messageType, type: messageType
maxStorageTime: maxStoreTime * TimeUtil.oneHourAsSeconds
}; };
if (maxStoreTime)
{
result.maxStorageTime = maxStoreTime * TimeUtil.oneHourAsSeconds;
}
return result;
} }
/** /**
* Add a templated message to the dialogue. * @deprecated Use MailSendService.sendMessage() or helpers
* @param dialogueID
* @param messageContent
* @param sessionID
* @param rewards
*/ */
public addDialogueMessage(dialogueID: string, messageContent: MessageContent, sessionID: string, rewards: Item[] = [], messageType = MessageType.NPC_TRADER): void public addDialogueMessage(dialogueID: string, messageContent: MessageContent, sessionID: string, rewards: Item[] = [], messageType = MessageType.NPC_TRADER): void
{ {
@ -129,7 +127,7 @@ export class DialogueHelper
dt: Math.round(Date.now() / 1000), dt: Math.round(Date.now() / 1000),
text: messageContent.text ?? "", text: messageContent.text ?? "",
templateId: messageContent.templateId, templateId: messageContent.templateId,
hasRewards: rewards.length > 0, hasRewards: items.data?.length > 0,
rewardCollected: false, rewardCollected: false,
items: items, items: items,
maxStorageTime: messageContent.maxStorageTime, maxStorageTime: messageContent.maxStorageTime,
@ -137,6 +135,11 @@ export class DialogueHelper
profileChangeEvents: (messageContent.profileChangeEvents?.length === 0) ? messageContent.profileChangeEvents : undefined profileChangeEvents: (messageContent.profileChangeEvents?.length === 0) ? messageContent.profileChangeEvents : undefined
}; };
if (!message.templateId)
{
delete message.templateId;
}
dialogue.messages.push(message); dialogue.messages.push(message);
// Offer Sold notifications are now separate from the main notification // Offer Sold notifications are now separate from the main notification

View File

@ -294,4 +294,39 @@ export class ProfileHelper
return profile; return profile;
} }
/**
* Flag a profile as having received a gift
* Store giftid in profile aki object
* @param playerId Player to add gift flag to
* @param giftId Gift player received
*/
public addGiftReceivedFlagToProfile(playerId: string, giftId: string): void
{
const profileToUpdate = this.getFullProfile(playerId);
const giftHistory = profileToUpdate.aki.receivedGifts;
if (!giftHistory)
{
profileToUpdate.aki.receivedGifts = [];
}
profileToUpdate.aki.receivedGifts.push({giftId: giftId, timestampAccepted: this.timeUtil.getTimestamp()});
}
/**
* Check if profile has recieved a gift by id
* @param playerId Player profile to check for gift
* @param giftId Gift to check for
* @returns True if player has recieved gift previously
*/
public playerHasRecievedGift(playerId: string, giftId: string): boolean
{
const profile = this.getFullProfile(playerId);
if (!profile.aki.receivedGifts)
{
return false;
}
return !!profile.aki.receivedGifts.find(x => x.giftId === giftId);
}
} }

View File

@ -386,4 +386,16 @@ export class TraderHelper
return this.highestTraderBuyPriceItems[tpl]; return this.highestTraderBuyPriceItems[tpl];
} }
/**
* Get a trader enum key by its value
* @param traderId Traders id
* @returns Traders key
*/
public getTraderById(traderId: string): Traders
{
const keys = Object.keys(Traders).filter(x => Traders[x] === traderId);
return keys.length > 0 ? keys[0] as Traders : null;
}
} }

View File

@ -81,7 +81,7 @@ export interface DialogueInfo
_id: string _id: string
type: MessageType type: MessageType
pinned: boolean pinned: boolean
Users?: any[] Users?: IUserDialogInfo[]
message: MessagePreview message: MessagePreview
} }
@ -97,7 +97,7 @@ export interface Message
text?: string text?: string
hasRewards: boolean hasRewards: boolean
rewardCollected: boolean rewardCollected: boolean
items: MessageItems items?: MessageItems
maxStorageTime?: number maxStorageTime?: number
systemData?: ISystemData systemData?: ISystemData
profileChangeEvents?: any[] profileChangeEvents?: any[]
@ -149,6 +149,7 @@ export interface Aki
{ {
version: string version: string
mods?: ModDetails[] mods?: ModDetails[]
receivedGifts: ReceivedGift[]
} }
export interface ModDetails export interface ModDetails
@ -159,6 +160,12 @@ export interface ModDetails
dateAdded: number dateAdded: number
} }
export interface ReceivedGift
{
giftId: string
timestampAccepted: number
}
export interface Vitality export interface Vitality
{ {
health: Health health: Health

View File

@ -22,5 +22,6 @@ export enum ConfigTypes
TRADER = "aki-trader", TRADER = "aki-trader",
WEATHER = "aki-weather", WEATHER = "aki-weather",
SEASONAL_EVENT = "aki-seasonalevents", SEASONAL_EVENT = "aki-seasonalevents",
LOST_ON_DEATH = "aki-lostondeath" LOST_ON_DEATH = "aki-lostondeath",
GIFTS = "aki-gifts"
} }

View File

@ -0,0 +1,6 @@
export enum GiftSenderType
{
SYSTEM = "System",
TRADER = "Trader",
USER = "User"
}

View File

@ -0,0 +1,7 @@
export enum GiftSentResult
{
FAILED_UNKNOWN = 1,
FAILED_GIFT_ALREADY_RECEIVED = 2,
FAILED_GIFT_DOESNT_EXIST = 3,
SUCCESS = 4
}

View File

@ -2,5 +2,7 @@ export enum SeasonalEventType
{ {
NONE = "None", NONE = "None",
CHRISTMAS = "Christmas", CHRISTMAS = "Christmas",
HALLOWEEN = "Halloween" HALLOWEEN = "Halloween",
NEW_YEARS = "NewYears",
PROMO = "Promo"
} }

View File

@ -0,0 +1,30 @@
import { Item } from "../../../models/eft/common/tables/IItem";
import { IUserDialogInfo } from "../../../models/eft/profile/IAkiProfile";
import { GiftSenderType } from "../../../models/enums/GiftSenderType";
import { SeasonalEventType } from "../../../models/enums/SeasonalEventType";
import { Traders } from "../../../models/enums/Traders";
import { IBaseConfig } from "./IBaseConfig";
export interface IGiftsConfig extends IBaseConfig
{
kind: "aki-gifts"
gifts: Record<string, Gift>
}
export interface Gift
{
/** Items to send to player */
items: Item[]
/** Who is sending the gift to player */
sender: GiftSenderType
/** Optinal - supply a users id to send from, not necessary when sending from SYSTEM or TRADER */
senderId?: string
senderDetails: IUserDialogInfo,
/** Optional - supply a trader type to send from, not necessary when sending from SYSTEM or USER */
trader?: Traders
messageText: string
/** Optional - Used by Seasonal events to send on specific day */
timestampToSend?: number
associatedEvent: SeasonalEventType
collectionTimeHours: number
}

View File

@ -6,6 +6,7 @@ import { IBaseConfig } from "./IBaseConfig";
export interface IQuestConfig extends IBaseConfig export interface IQuestConfig extends IBaseConfig
{ {
kind: "aki-quest" kind: "aki-quest"
// Hours to get/redeem items from quest mail
redeemTime: number redeemTime: number
questTemplateIds: IPlayerTypeQuestIds questTemplateIds: IPlayerTypeQuestIds
/** Show non-seasonal quests be shown to player */ /** Show non-seasonal quests be shown to player */

View File

@ -0,0 +1,32 @@
import { Item } from "../../../models/eft/common/tables/IItem";
import { ISystemData, IUserDialogInfo, MessageContentRagfair } from "../../../models/eft/profile/IAkiProfile";
import { MessageType } from "../../../models/enums/MessageType";
import { Traders } from "../../../models/enums/Traders";
export interface ISendMessageDetails
{
/** Player id */
recipientId: string
/** Who is sending this message */
sender: MessageType
/** Optional - leave blank to use sender value */
dialogType?: MessageType
/** Optional - if sender is USER these details are used */
senderDetails?: IUserDialogInfo
/** Optional - the trader sending the message */
trader?: Traders
/** Optional - used in player/system messages, otherwise templateId is used */
messageText?: string
/** Optinal - Items to send to player */
items?: Item[];
/** Optional - How long items will be stored in mail before expiry */
itemsMaxStorageLifetimeSeconds?: number
/** Optional - Used when sending messages from traders who send text from locale json */
templateId?: string
/** Optional - ragfair related */
systemData?: ISystemData
/** Optional - Used by ragfair messages */
ragfairDetails?: MessageContentRagfair
/** Optional - Usage not known, unsure of purpose, even dumps dont have it */
profileChangeEvents?: any[]
}

View File

@ -1,37 +1,156 @@
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
import { DialogueHelper } from "../helpers/DialogueHelper"; import { ProfileHelper } from "../helpers/ProfileHelper";
import { ConfigTypes } from "../models/enums/ConfigTypes"; import { ConfigTypes } from "../models/enums/ConfigTypes";
import { GiftSenderType } from "../models/enums/GiftSenderType";
import { GiftSentResult } from "../models/enums/GiftSentResult";
import { MessageType } from "../models/enums/MessageType"; import { MessageType } from "../models/enums/MessageType";
import { Gift, IGiftsConfig } from "../models/spt/config/IGiftsConfig";
import { ISendMessageDetails } from "../models/spt/dialog/ISendMessageDetails";
import { ILogger } from "../models/spt/utils/ILogger"; import { ILogger } from "../models/spt/utils/ILogger";
import { ConfigServer } from "../servers/ConfigServer"; import { ConfigServer } from "../servers/ConfigServer";
import { HashUtil } from "../utils/HashUtil";
import { TimeUtil } from "../utils/TimeUtil";
import { MailSendService } from "./MailSendService";
@injectable() @injectable()
export class GiftService export class GiftService
{ {
protected giftConfig: any; protected giftConfig: IGiftsConfig;
constructor( constructor(
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper, @inject("MailSendService") protected mailSendService: MailSendService,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer
) )
{ {
this.giftConfig = this.configServer.getConfig(ConfigTypes.QUEST); this.giftConfig = this.configServer.getConfig(ConfigTypes.GIFTS);
} }
/** /**
* Send a player a gift * Does a gift with a specific ID exist in db
* @param playerId Player to send gift to * @param giftId Gift id to check for
* @param giftId Id of gift to send player * @returns True if it exists in db
*/ */
public sendGiftToPlayer(playerId: string, giftId: string): void public giftExists(giftId: string): boolean
{ {
//TODO: get gift items return !!this.giftConfig.gifts[giftId];
const giftItems = []; }
const maxStoreTime = 999999;
const messageContent = this.dialogueHelper.createMessageContext(null, MessageType.SYSTEM_MESSAGE, maxStoreTime); /**
* Send player a gift from a range of sources
* @param playerId Player to send gift to / sessionId
* @param giftId Id of gift in configs/gifts.json to send player
* @returns outcome of sending gift to player
*/
public sendGiftToPlayer(playerId: string, giftId: string): GiftSentResult
{
const giftData = this.giftConfig.gifts[giftId];
if (!giftData)
{
return GiftSentResult.FAILED_GIFT_DOESNT_EXIST;
}
this.dialogueHelper.addDialogueMessage("traderId", messageContent, playerId, giftItems, MessageType.SYSTEM_MESSAGE); if (this.profileHelper.playerHasRecievedGift(playerId, giftId))
{
this.logger.debug(`Player already recieved gift: ${giftId}`);
return GiftSentResult.FAILED_GIFT_ALREADY_RECEIVED;
}
// Handle system messsages
if (giftData.sender === GiftSenderType.SYSTEM)
{
this.mailSendService.sendSystemMessageToPlayer(
playerId,
giftData.messageText,
giftData.items,
this.timeUtil.getHoursAsSeconds(giftData.collectionTimeHours));
}
// Handle user messages
else if (giftData.sender === GiftSenderType.USER)
{
this.mailSendService.sendUserMessageToPlayer(
playerId,
giftData.senderDetails,
giftData.messageText,
giftData.items,
this.timeUtil.getHoursAsSeconds(giftData.collectionTimeHours));
}
else if (giftData.sender === GiftSenderType.TRADER)
{
this.mailSendService.sendDirectNpcMessageToPlayer(
playerId,
giftData.trader,
MessageType.MESSAGE_WITH_ITEMS,
giftData.messageText,
giftData.items,
this.timeUtil.getHoursAsSeconds(giftData.collectionTimeHours));
}
else
{
// TODO: further split out into different message systems like above SYSTEM method
// Trader / ragfair
const details: ISendMessageDetails = {
recipientId: playerId,
sender: this.getMessageType(giftData),
senderDetails: { _id: this.getSenderId(giftData), info: null},
messageText: giftData.messageText,
items: giftData.items,
itemsMaxStorageLifetimeSeconds: this.timeUtil.getHoursAsSeconds(giftData.collectionTimeHours)
};
if (giftData.trader)
{
details.trader = giftData.trader;
}
this.mailSendService.sendMessageToPlayer(details);
}
this.profileHelper.addGiftReceivedFlagToProfile(playerId, giftId);
return GiftSentResult.SUCCESS;
}
/**
* Get sender id based on gifts sender type enum
* @param giftData Gift to send player
* @returns trader/user/system id
*/
protected getSenderId(giftData: Gift): string
{
if (giftData.sender === GiftSenderType.TRADER)
{
return giftData.trader;
}
if (giftData.sender === GiftSenderType.USER)
{
return giftData.senderId;
}
}
/**
* Convert GiftSenderType into a dialog MessageType
* @param giftData Gift to send player
* @returns MessageType enum value
*/
protected getMessageType(giftData: Gift): MessageType
{
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:
this.logger.error(`Gift message type: ${giftData.sender} not handled`);
break;
}
} }
} }

View File

@ -0,0 +1,408 @@
import { inject, injectable } from "tsyringe";
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("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
{
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
{
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.saveServer.getProfile(messageDetails.recipientId).dialogues;
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)
{
return 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}`);
}
}

View File

@ -105,7 +105,8 @@ export class ProfileFixerService
{ {
this.logger.debug("Adding aki object to profile"); this.logger.debug("Adding aki object to profile");
fullProfile.aki = { fullProfile.aki = {
version: this.watermark.getVersionTag() version: this.watermark.getVersionTag(),
receivedGifts: []
}; };
} }
} }