From a7b4ebe31666ae8320e052f4c4932277876c879d Mon Sep 17 00:00:00 2001 From: chomp Date: Fri, 21 Jul 2023 17:08:32 +0000 Subject: [PATCH] Rework message sending to support gift system (!106) Co-authored-by: Kaeno Co-authored-by: Dev Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/106 --- project/assets/configs/gifts.json | 570 ++++++++++++++++++ project/src/controllers/DialogueController.ts | 115 ++-- project/src/controllers/GameController.ts | 2 +- project/src/controllers/QuestController.ts | 13 +- project/src/di/Container.ts | 2 + project/src/generators/BotGenerator.ts | 4 +- project/src/generators/BotWeaponGenerator.ts | 2 +- .../src/helpers/BotWeaponGeneratorHelper.ts | 1 + project/src/helpers/DialogueHelper.ts | 33 +- project/src/helpers/ProfileHelper.ts | 35 ++ project/src/helpers/TraderHelper.ts | 12 + project/src/models/eft/profile/IAkiProfile.ts | 11 +- project/src/models/enums/ConfigTypes.ts | 3 +- project/src/models/enums/GiftSenderType.ts | 6 + project/src/models/enums/GiftSentResult.ts | 7 + project/src/models/enums/SeasonalEventType.ts | 4 +- project/src/models/spt/config/IGiftsConfig.ts | 30 + project/src/models/spt/config/IQuestConfig.ts | 1 + .../models/spt/dialog/ISendMessageDetails.ts | 32 + project/src/services/GiftService.ts | 145 ++++- project/src/services/MailSendService.ts | 408 +++++++++++++ project/src/services/ProfileFixerService.ts | 3 +- 22 files changed, 1361 insertions(+), 78 deletions(-) create mode 100644 project/assets/configs/gifts.json create mode 100644 project/src/models/enums/GiftSenderType.ts create mode 100644 project/src/models/enums/GiftSentResult.ts create mode 100644 project/src/models/spt/config/IGiftsConfig.ts create mode 100644 project/src/models/spt/dialog/ISendMessageDetails.ts create mode 100644 project/src/services/MailSendService.ts diff --git a/project/assets/configs/gifts.json b/project/assets/configs/gifts.json new file mode 100644 index 00000000..5cd49fea --- /dev/null +++ b/project/assets/configs/gifts.json @@ -0,0 +1,570 @@ +{ + "gifts": { + "NewYear2021": { + "sender": "System", + "messageText": "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!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!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!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!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!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!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!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!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!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!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!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!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!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!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!Ну, здравствуй, друг! С Новым годом тебя! Ты наверное забыл уже, что есть праздники, но я считаю, важно оставаться людьми в любой ситуации. Работающих видеокарт тебе в новом году и убежище чтобы не подводило. Держи и радуйся!", + "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" + } + } +} \ No newline at end of file diff --git a/project/src/controllers/DialogueController.ts b/project/src/controllers/DialogueController.ts index 7a0282ae..d3116fbd 100644 --- a/project/src/controllers/DialogueController.ts +++ b/project/src/controllers/DialogueController.ts @@ -1,5 +1,6 @@ import { inject, injectable } from "tsyringe"; +import { GiftSentResult } from "@spt-aki/models/enums/GiftSentResult"; import { DialogueHelper } from "../helpers/DialogueHelper"; import { IGetAllAttachmentsResponse } from "../models/eft/dialog/IGetAllAttachmentsResponse"; 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 { MemberCategory } from "../models/enums/MemberCategory"; import { MessageType } from "../models/enums/MessageType"; +import { ILogger } from "../models/spt/utils/ILogger"; import { SaveServer } from "../servers/SaveServer"; +import { GiftService } from "../services/GiftService"; +import { MailSendService } from "../services/MailSendService"; import { HashUtil } from "../utils/HashUtil"; import { TimeUtil } from "../utils/TimeUtil"; @@ -19,12 +23,15 @@ import { TimeUtil } from "../utils/TimeUtil"; export class DialogueController { constructor( + @inject("WinstonLogger") protected logger: ILogger, @inject("SaveServer") protected saveServer: SaveServer, @inject("TimeUtil") protected timeUtil: TimeUtil, @inject("DialogueHelper") protected dialogueHelper: DialogueHelper, + @inject("MailSendService") protected mailSendService: MailSendService, + @inject("GiftService") protected giftService: GiftService, @inject("HashUtil") protected hashUtil: HashUtil ) - { } + {} /** Handle onUpdate spt event */ public update(): void @@ -46,7 +53,7 @@ export class DialogueController return { "Friends": [ { - _id: "sptfriend", + _id: "sptFriend", Info: { Level: 1, MemberCategory: MemberCategory.DEVELOPER, @@ -89,25 +96,36 @@ export class DialogueController const dialogue = this.saveServer.getProfile(sessionID).dialogues[dialogueID]; const result: DialogueInfo = { - "_id": dialogueID, - "type": dialogue.type ? dialogue.type : MessageType.NPC_TRADER, - "message": this.dialogueHelper.getMessagePreview(dialogue), - "new": dialogue.new, - "attachmentsNew": dialogue.attachmentsNew, - "pinned": dialogue.pinned, - Users: this.getDialogueUsers(dialogue.Users, dialogue.type, sessionID) + _id: dialogueID, + type: dialogue.type ? dialogue.type : MessageType.NPC_TRADER, + message: this.dialogueHelper.getMessagePreview(dialogue), + new: dialogue.new, + attachmentsNew: dialogue.attachmentsNew, + pinned: dialogue.pinned, + Users: this.getDialogueUsers(dialogue, dialogue.type, sessionID) }; 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); - 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, info: { 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]; } - + /** + * TODO + * @param pmcProfile + * @param dialogUsers + * @returns + */ protected getProfilesForMail(pmcProfile: IAkiProfile, dialogUsers: IUserDialogInfo[]): IUserDialogInfo[] { const result: IUserDialogInfo[] = []; @@ -285,37 +308,51 @@ export class DialogueController // eslint-disable-next-line @typescript-eslint/no-unused-vars public sendMessage(sessionId: string, request: ISendMessageRequest): string { - const profile = this.saveServer.getProfile(sessionId); - 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 - }); + this.mailSendService.sendPlayerMessageToNpc(sessionId, request.dialogId, 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({ - _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; + this.handleChatWithSPTFriend(sessionId, request); } 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 * @param sessionId Session id diff --git a/project/src/controllers/GameController.ts b/project/src/controllers/GameController.ts index e3a7473e..4fae7b25 100644 --- a/project/src/controllers/GameController.ts +++ b/project/src/controllers/GameController.ts @@ -584,7 +584,7 @@ export class GameController if (!traderAssorts.loyal_level_items[assortKey]) { // 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})); } } } diff --git a/project/src/controllers/QuestController.ts b/project/src/controllers/QuestController.ts index 557a1734..1c58eec7 100644 --- a/project/src/controllers/QuestController.ts +++ b/project/src/controllers/QuestController.ts @@ -5,6 +5,7 @@ import { ItemHelper } from "../helpers/ItemHelper"; import { ProfileHelper } from "../helpers/ProfileHelper"; import { QuestConditionHelper } from "../helpers/QuestConditionHelper"; import { QuestHelper } from "../helpers/QuestHelper"; +import { TraderHelper } from "../helpers/TraderHelper"; import { IPmcData } from "../models/eft/common/IPmcData"; import { Quest } from "../models/eft/common/tables/IBotBase"; import { Item } from "../models/eft/common/tables/IItem"; @@ -26,6 +27,7 @@ import { ConfigServer } from "../servers/ConfigServer"; import { DatabaseServer } from "../servers/DatabaseServer"; import { LocaleService } from "../services/LocaleService"; import { LocalisationService } from "../services/LocalisationService"; +import { MailSendService } from "../services/MailSendService"; import { PlayerService } from "../services/PlayerService"; import { SeasonalEventService } from "../services/SeasonalEventService"; import { HttpResponseUtil } from "../utils/HttpResponseUtil"; @@ -44,7 +46,9 @@ export class QuestController @inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("ItemHelper") protected itemHelper: ItemHelper, @inject("DialogueHelper") protected dialogueHelper: DialogueHelper, + @inject("MailSendService") protected mailSendService: MailSendService, @inject("ProfileHelper") protected profileHelper: ProfileHelper, + @inject("TraderHelper") protected traderHelper: TraderHelper, @inject("QuestHelper") protected questHelper: QuestHelper, @inject("QuestConditionHelper") protected questConditionHelper: QuestConditionHelper, @inject("PlayerService") protected playerService: PlayerService, @@ -427,9 +431,14 @@ export class QuestController protected sendSuccessDialogMessageOnQuestComplete(sessionID: string, pmcData: IPmcData, completedQuestId: string, questRewards: Reward[]): void { 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)); } /** diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index b4942e15..ae96cafe 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -201,6 +201,7 @@ import { ItemBaseClassService } from "../services/ItemBaseClassService"; import { ItemFilterService } from "../services/ItemFilterService"; import { LocaleService } from "../services/LocaleService"; import { LocalisationService } from "../services/LocalisationService"; +import { MailSendService } from "../services/MailSendService"; import { MatchBotDetailsCacheService } from "../services/MatchBotDetailsCacheService"; import { MatchLocationService } from "../services/MatchLocationService"; import { ModCompilerService } from "../services/ModCompilerService"; @@ -610,6 +611,7 @@ export class Container depContainer.register("TraderPurchasePersisterService", TraderPurchasePersisterService); depContainer.register("PmcChatResponseService", PmcChatResponseService); depContainer.register("GiftService", GiftService); + depContainer.register("MailSendService", MailSendService); } private static registerServers(depContainer: DependencyContainer): void diff --git a/project/src/generators/BotGenerator.ts b/project/src/generators/BotGenerator.ts index 130040b4..6678cbb1 100644 --- a/project/src/generators/BotGenerator.ts +++ b/project/src/generators/BotGenerator.ts @@ -429,13 +429,13 @@ export class BotGenerator if (botInfo.Nickname.toLowerCase() === "nikita") { botInfo.GameVersion = "edge_of_darkness"; - botInfo.AccountType = MemberCategory.DEVELOPER; + botInfo.MemberCategory = MemberCategory.DEVELOPER; return; } 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)); } /** diff --git a/project/src/generators/BotWeaponGenerator.ts b/project/src/generators/BotWeaponGenerator.ts index bea2411f..4400aa30 100644 --- a/project/src/generators/BotWeaponGenerator.ts +++ b/project/src/generators/BotWeaponGenerator.ts @@ -356,7 +356,7 @@ export class BotWeaponGenerator this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot([EquipmentSlots.SECURED_CONTAINER], id, ammoTpl, [{ _id: id, _tpl: ammoTpl, - upd: { "StackObjectsCount": stackSize } + upd: { StackObjectsCount: stackSize } }], inventory); } diff --git a/project/src/helpers/BotWeaponGeneratorHelper.ts b/project/src/helpers/BotWeaponGeneratorHelper.ts index 0051bb91..3118964a 100644 --- a/project/src/helpers/BotWeaponGeneratorHelper.ts +++ b/project/src/helpers/BotWeaponGeneratorHelper.ts @@ -162,6 +162,7 @@ export class BotWeaponGeneratorHelper if (!container) { // Desired equipment container (e.g. backpack) not found + this.logger.warning(`Unable to add items to bot slot: ${slot}, slot missing`); continue; } diff --git a/project/src/helpers/DialogueHelper.ts b/project/src/helpers/DialogueHelper.ts index a656ed47..a32332fa 100644 --- a/project/src/helpers/DialogueHelper.ts +++ b/project/src/helpers/DialogueHelper.ts @@ -29,27 +29,25 @@ export class DialogueHelper { } /** - * Create basic message context template - * @param templateId - * @param messageType - * @param maxStoreTime - * @returns + * @deprecated Use MailSendService.sendMessage() or helpers */ - public createMessageContext(templateId: string, messageType: MessageType, maxStoreTime: number): MessageContent + public createMessageContext(templateId: string, messageType: MessageType, maxStoreTime = null): MessageContent { - return { + const result: MessageContent = { templateId: templateId, - type: messageType, - maxStorageTime: maxStoreTime * TimeUtil.oneHourAsSeconds + type: messageType }; + + if (maxStoreTime) + { + result.maxStorageTime = maxStoreTime * TimeUtil.oneHourAsSeconds; + } + + return result; } /** - * Add a templated message to the dialogue. - * @param dialogueID - * @param messageContent - * @param sessionID - * @param rewards + * @deprecated Use MailSendService.sendMessage() or helpers */ 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), text: messageContent.text ?? "", templateId: messageContent.templateId, - hasRewards: rewards.length > 0, + hasRewards: items.data?.length > 0, rewardCollected: false, items: items, maxStorageTime: messageContent.maxStorageTime, @@ -137,6 +135,11 @@ export class DialogueHelper profileChangeEvents: (messageContent.profileChangeEvents?.length === 0) ? messageContent.profileChangeEvents : undefined }; + if (!message.templateId) + { + delete message.templateId; + } + dialogue.messages.push(message); // Offer Sold notifications are now separate from the main notification diff --git a/project/src/helpers/ProfileHelper.ts b/project/src/helpers/ProfileHelper.ts index dbc22b3d..e7747e0b 100644 --- a/project/src/helpers/ProfileHelper.ts +++ b/project/src/helpers/ProfileHelper.ts @@ -294,4 +294,39 @@ export class ProfileHelper 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); + } } \ No newline at end of file diff --git a/project/src/helpers/TraderHelper.ts b/project/src/helpers/TraderHelper.ts index 8131422f..2d810377 100644 --- a/project/src/helpers/TraderHelper.ts +++ b/project/src/helpers/TraderHelper.ts @@ -386,4 +386,16 @@ export class TraderHelper 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; + } } \ No newline at end of file diff --git a/project/src/models/eft/profile/IAkiProfile.ts b/project/src/models/eft/profile/IAkiProfile.ts index 794b1594..018bcb16 100644 --- a/project/src/models/eft/profile/IAkiProfile.ts +++ b/project/src/models/eft/profile/IAkiProfile.ts @@ -81,7 +81,7 @@ export interface DialogueInfo _id: string type: MessageType pinned: boolean - Users?: any[] + Users?: IUserDialogInfo[] message: MessagePreview } @@ -97,7 +97,7 @@ export interface Message text?: string hasRewards: boolean rewardCollected: boolean - items: MessageItems + items?: MessageItems maxStorageTime?: number systemData?: ISystemData profileChangeEvents?: any[] @@ -149,6 +149,7 @@ export interface Aki { version: string mods?: ModDetails[] + receivedGifts: ReceivedGift[] } export interface ModDetails @@ -159,6 +160,12 @@ export interface ModDetails dateAdded: number } +export interface ReceivedGift +{ + giftId: string + timestampAccepted: number +} + export interface Vitality { health: Health diff --git a/project/src/models/enums/ConfigTypes.ts b/project/src/models/enums/ConfigTypes.ts index 23252e33..615ec672 100644 --- a/project/src/models/enums/ConfigTypes.ts +++ b/project/src/models/enums/ConfigTypes.ts @@ -22,5 +22,6 @@ export enum ConfigTypes TRADER = "aki-trader", WEATHER = "aki-weather", SEASONAL_EVENT = "aki-seasonalevents", - LOST_ON_DEATH = "aki-lostondeath" + LOST_ON_DEATH = "aki-lostondeath", + GIFTS = "aki-gifts" } \ No newline at end of file diff --git a/project/src/models/enums/GiftSenderType.ts b/project/src/models/enums/GiftSenderType.ts new file mode 100644 index 00000000..793d0bd3 --- /dev/null +++ b/project/src/models/enums/GiftSenderType.ts @@ -0,0 +1,6 @@ +export enum GiftSenderType + { + SYSTEM = "System", + TRADER = "Trader", + USER = "User" +} \ No newline at end of file diff --git a/project/src/models/enums/GiftSentResult.ts b/project/src/models/enums/GiftSentResult.ts new file mode 100644 index 00000000..7c1295dd --- /dev/null +++ b/project/src/models/enums/GiftSentResult.ts @@ -0,0 +1,7 @@ +export enum GiftSentResult + { + FAILED_UNKNOWN = 1, + FAILED_GIFT_ALREADY_RECEIVED = 2, + FAILED_GIFT_DOESNT_EXIST = 3, + SUCCESS = 4 +} \ No newline at end of file diff --git a/project/src/models/enums/SeasonalEventType.ts b/project/src/models/enums/SeasonalEventType.ts index 369a8261..158b4df8 100644 --- a/project/src/models/enums/SeasonalEventType.ts +++ b/project/src/models/enums/SeasonalEventType.ts @@ -2,5 +2,7 @@ export enum SeasonalEventType { NONE = "None", CHRISTMAS = "Christmas", - HALLOWEEN = "Halloween" + HALLOWEEN = "Halloween", + NEW_YEARS = "NewYears", + PROMO = "Promo" } \ No newline at end of file diff --git a/project/src/models/spt/config/IGiftsConfig.ts b/project/src/models/spt/config/IGiftsConfig.ts new file mode 100644 index 00000000..21d33bf5 --- /dev/null +++ b/project/src/models/spt/config/IGiftsConfig.ts @@ -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 +} + +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 +} \ No newline at end of file diff --git a/project/src/models/spt/config/IQuestConfig.ts b/project/src/models/spt/config/IQuestConfig.ts index e029e7e0..34f27427 100644 --- a/project/src/models/spt/config/IQuestConfig.ts +++ b/project/src/models/spt/config/IQuestConfig.ts @@ -6,6 +6,7 @@ import { IBaseConfig } from "./IBaseConfig"; export interface IQuestConfig extends IBaseConfig { kind: "aki-quest" + // Hours to get/redeem items from quest mail redeemTime: number questTemplateIds: IPlayerTypeQuestIds /** Show non-seasonal quests be shown to player */ diff --git a/project/src/models/spt/dialog/ISendMessageDetails.ts b/project/src/models/spt/dialog/ISendMessageDetails.ts new file mode 100644 index 00000000..35a287a6 --- /dev/null +++ b/project/src/models/spt/dialog/ISendMessageDetails.ts @@ -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[] +} diff --git a/project/src/services/GiftService.ts b/project/src/services/GiftService.ts index bcceca43..431d4ba0 100644 --- a/project/src/services/GiftService.ts +++ b/project/src/services/GiftService.ts @@ -1,37 +1,156 @@ import { inject, injectable } from "tsyringe"; -import { DialogueHelper } from "../helpers/DialogueHelper"; +import { ProfileHelper } from "../helpers/ProfileHelper"; 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 { Gift, IGiftsConfig } from "../models/spt/config/IGiftsConfig"; +import { ISendMessageDetails } from "../models/spt/dialog/ISendMessageDetails"; import { ILogger } from "../models/spt/utils/ILogger"; import { ConfigServer } from "../servers/ConfigServer"; +import { HashUtil } from "../utils/HashUtil"; +import { TimeUtil } from "../utils/TimeUtil"; +import { MailSendService } from "./MailSendService"; @injectable() export class GiftService { - protected giftConfig: any; + protected giftConfig: IGiftsConfig; constructor( @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 ) { - this.giftConfig = this.configServer.getConfig(ConfigTypes.QUEST); + this.giftConfig = this.configServer.getConfig(ConfigTypes.GIFTS); } /** - * Send a player a gift - * @param playerId Player to send gift to - * @param giftId Id of gift to send player + * Does a gift with a specific ID exist in db + * @param giftId Gift id to check for + * @returns True if it exists in db */ - public sendGiftToPlayer(playerId: string, giftId: string): void + public giftExists(giftId: string): boolean { - //TODO: get gift items - const giftItems = []; - const maxStoreTime = 999999; + return !!this.giftConfig.gifts[giftId]; + } - 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; + } } } \ No newline at end of file diff --git a/project/src/services/MailSendService.ts b/project/src/services/MailSendService.ts new file mode 100644 index 00000000..4fa09d68 --- /dev/null +++ b/project/src/services/MailSendService.ts @@ -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}`); + } + +} \ No newline at end of file diff --git a/project/src/services/ProfileFixerService.ts b/project/src/services/ProfileFixerService.ts index 4428807f..e6937b29 100644 --- a/project/src/services/ProfileFixerService.ts +++ b/project/src/services/ProfileFixerService.ts @@ -105,7 +105,8 @@ export class ProfileFixerService { this.logger.debug("Adding aki object to profile"); fullProfile.aki = { - version: this.watermark.getVersionTag() + version: this.watermark.getVersionTag(), + receivedGifts: [] }; } }