From c7572fdaf28f179280a89f31af875265c7af36f0 Mon Sep 17 00:00:00 2001 From: Dev Date: Thu, 21 Dec 2023 22:12:55 +0000 Subject: [PATCH 01/10] Improve accuracy of total game time --- project/src/controllers/GameController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project/src/controllers/GameController.ts b/project/src/controllers/GameController.ts index 4a68c300..1f9af061 100644 --- a/project/src/controllers/GameController.ts +++ b/project/src/controllers/GameController.ts @@ -420,6 +420,7 @@ export class GameController public getGameConfig(sessionID: string): IGameConfigResponse { const profile = this.profileHelper.getPmcProfile(sessionID); + const gameTime = profile.Stats?.Eft.OverallCounters.Items?.find(counter => counter.Key.includes("LifeTime") && counter.Key.includes("Pmc"))?.Value ?? 0; const config: IGameConfigResponse = { languages: this.databaseServer.getTables().locales.languages, @@ -440,7 +441,7 @@ export class GameController useProtobuf: false, // eslint-disable-next-line @typescript-eslint/naming-convention utc_time: new Date().getTime() / 1000, - totalInGame: profile.Stats?.Eft?.TotalInGameTime ?? 0, + totalInGame: gameTime, }; return config; From 66f04f194a686a0340d373f45f8515c18cefa4c5 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 22 Dec 2023 09:27:15 +0000 Subject: [PATCH 02/10] Added config to compress profile (zero to none performance difference on high end CPUs) and debug time measurements for load/save times (!178) Co-authored-by: clodan Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/178 Co-authored-by: Alex Co-committed-by: Alex --- project/assets/configs/core.json | 3 ++- project/src/models/spt/config/ICoreConfig.ts | 1 + project/src/servers/SaveServer.ts | 10 +++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/project/assets/configs/core.json b/project/assets/configs/core.json index 56cffc84..4f4f8bd9 100644 --- a/project/assets/configs/core.json +++ b/project/assets/configs/core.json @@ -11,6 +11,7 @@ "fixProfileBreakingInventoryItemIssues": false }, "features": { - "autoInstallModDependencies": false + "autoInstallModDependencies": false, + "compressProfile": false } } diff --git a/project/src/models/spt/config/ICoreConfig.ts b/project/src/models/spt/config/ICoreConfig.ts index 3d9c6319..13cebaec 100644 --- a/project/src/models/spt/config/ICoreConfig.ts +++ b/project/src/models/spt/config/ICoreConfig.ts @@ -31,4 +31,5 @@ export interface IServerFeatures { /* Controls whether or not the server attempts to download mod dependencies not included in the server's executable */ autoInstallModDependencies: boolean; + compressProfile: boolean; } diff --git a/project/src/servers/SaveServer.ts b/project/src/servers/SaveServer.ts index d5c2beba..527ececf 100644 --- a/project/src/servers/SaveServer.ts +++ b/project/src/servers/SaveServer.ts @@ -7,6 +7,9 @@ import { LocalisationService } from "@spt-aki/services/LocalisationService"; import { HashUtil } from "@spt-aki/utils/HashUtil"; import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { VFS } from "@spt-aki/utils/VFS"; +import { ConfigServer } from "./ConfigServer"; +import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; +import { ICoreConfig } from "@spt-aki/models/spt/config/ICoreConfig"; @injectable() export class SaveServer @@ -24,6 +27,7 @@ export class SaveServer @inject("HashUtil") protected hashUtil: HashUtil, @inject("LocalisationService") protected localisationService: LocalisationService, @inject("WinstonLogger") protected logger: ILogger, + @inject("ConfigServer") protected configServer: ConfigServer ) {} @@ -166,7 +170,9 @@ export class SaveServer if (this.vfs.exists(filePath)) { // File found, store in profiles[] + const start = performance.now(); this.profiles[sessionID] = this.jsonUtil.deserialize(this.vfs.readFile(filePath), filename); + this.logger.debug(`Profile ${sessionID} took ${performance.now() - start}ms to load.`); } // Run callbacks @@ -200,7 +206,8 @@ export class SaveServer } } - const jsonProfile = this.jsonUtil.serialize(this.profiles[sessionID], true); + const start = performance.now(); + const jsonProfile = this.jsonUtil.serialize(this.profiles[sessionID], !this.configServer.getConfig(ConfigTypes.CORE).features.compressProfile); const fmd5 = this.hashUtil.generateMd5ForData(jsonProfile); if (typeof (this.saveMd5[sessionID]) !== "string" || this.saveMd5[sessionID] !== fmd5) { @@ -209,6 +216,7 @@ export class SaveServer this.vfs.writeFile(filePath, jsonProfile); this.logger.debug(this.localisationService.getText("profile_saved", sessionID), true); } + this.logger.debug(`Profile ${sessionID} took ${performance.now() - start}ms to save.`); } /** From ea2257c2fb28c58e3ba3f338a23ad78ba9b97fca Mon Sep 17 00:00:00 2001 From: Dev Date: Sun, 24 Dec 2023 16:06:18 +0000 Subject: [PATCH 03/10] Add function to allow addition of custom weapons to PMCs --- project/src/services/mod/CustomItemService.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/project/src/services/mod/CustomItemService.ts b/project/src/services/mod/CustomItemService.ts index 507fb962..2e73f7bb 100644 --- a/project/src/services/mod/CustomItemService.ts +++ b/project/src/services/mod/CustomItemService.ts @@ -1,5 +1,6 @@ import { inject, injectable } from "tsyringe"; +import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { ITemplateItem, Props } from "@spt-aki/models/eft/common/tables/ITemplateItem"; import { CreateItemResult, @@ -23,6 +24,7 @@ export class CustomItemService @inject("HashUtil") protected hashUtil: HashUtil, @inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("DatabaseServer") protected databaseServer: DatabaseServer, + @inject("ItemHelper") protected itemHelper: ItemHelper, ) { this.tables = this.databaseServer.getTables(); @@ -197,4 +199,41 @@ export class CustomItemService { this.tables.templates.prices[newItemId] = fleaPriceRoubles; } + + /** + * Add a custom weapon to PMCs loadout + * @param weaponTpl Custom weapon tpl to add to PMCs + * @param weaponWeight The weighting for the weapon to be picked vs other weapons + * @param weaponSlot The slot the weapon should be added to (e.g. FirstPrimaryWeapon/SecondPrimaryWeapon/Holster) + */ + public addCustomWeaponToPMCs(weaponTpl: string, weaponWeight: number, weaponSlot: string): void + { + const weapon = this.itemHelper.getItem(weaponTpl); + if (!weapon[0]) + { + this.logger.warning(`Unable to add custom weapon ${weaponTpl} to PMCs as it cannot be found in the Item db`); + + return; + } + const baseWeaponModObject = {}; + + // Get all slots weapon has and create a dictionary of them with possible mods that slot into each + const weaponSltos = weapon[1]._props.Slots; + for (const slot of weaponSltos) + { + baseWeaponModObject[slot._name] = slot._props.filters[0].Filter; + } + + // Get PMCs + const usec = this.databaseServer.getTables().bots.types.usec; + const bear = this.databaseServer.getTables().bots.types.bear; + + // Add weapon base+mods into bear/usec data + usec.inventory.mods[weaponTpl] = baseWeaponModObject; + bear.inventory.mods[weaponTpl] = baseWeaponModObject; + + // Add weapon to array of allowed weapons + weighting to be picked + usec.inventory.equipment[weaponSlot][weaponTpl] = weaponWeight; + bear.inventory.equipment[weaponSlot][weaponTpl] = weaponWeight; + } } From 26a6553eaafef609719a7c186f10bf00bcb72d80 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 24 Dec 2023 19:54:27 +0000 Subject: [PATCH 04/10] Add customizable chat bots and chat commands (!179) * Use ICommandoCommand interface to register a new command for Commando! Our new and shiny chat bot that takes care of all your commanding needs * Use IDialogueChatBot to register you new chatty friend bot! * If you are feeling lazy, you can also use the ISptCommand and register a command that will use "spt" prefix * spt give command has been added! Feeling like cheating today? hehe use "spt give tplId quantity" and get a new shiny item on your inbox! Co-authored-by: clodan Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/179 Co-authored-by: Alex Co-committed-by: Alex --- project/.eslintrc.json | 1 + project/src/callbacks/DialogueCallbacks.ts | 4 +- project/src/controllers/DialogueController.ts | 174 ++++-------------- project/src/di/Container.ts | 23 +++ .../Dialogue/Commando/ICommandoAction.ts | 7 + .../Dialogue/Commando/ICommandoCommand.ts | 9 + .../Dialogue/Commando/SptCommandoCommands.ts | 38 ++++ .../Commando/SptCommands/GiveSptCommand.ts | 128 +++++++++++++ .../Commando/SptCommands/ISptCommand.ts | 7 + .../Dialogue/CommandoDialogueChatBot.ts | 88 +++++++++ .../src/helpers/Dialogue/IDialogueChatBot.ts | 8 + .../helpers/Dialogue/SptDialogueChatBot.ts | 151 +++++++++++++++ project/src/utils/HttpResponseUtil.ts | 14 +- 13 files changed, 501 insertions(+), 151 deletions(-) create mode 100644 project/src/helpers/Dialogue/Commando/ICommandoAction.ts create mode 100644 project/src/helpers/Dialogue/Commando/ICommandoCommand.ts create mode 100644 project/src/helpers/Dialogue/Commando/SptCommandoCommands.ts create mode 100644 project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts create mode 100644 project/src/helpers/Dialogue/Commando/SptCommands/ISptCommand.ts create mode 100644 project/src/helpers/Dialogue/CommandoDialogueChatBot.ts create mode 100644 project/src/helpers/Dialogue/IDialogueChatBot.ts create mode 100644 project/src/helpers/Dialogue/SptDialogueChatBot.ts diff --git a/project/.eslintrc.json b/project/.eslintrc.json index 54b23e15..72de65ff 100644 --- a/project/.eslintrc.json +++ b/project/.eslintrc.json @@ -11,6 +11,7 @@ "plugin:@typescript-eslint/eslint-recommended" ], "rules": { + "brace-style": ["error", "allman"], "@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-explicit-any": "off", // We use a bunch of these. diff --git a/project/src/callbacks/DialogueCallbacks.ts b/project/src/callbacks/DialogueCallbacks.ts index 804e7344..aef83abf 100644 --- a/project/src/callbacks/DialogueCallbacks.ts +++ b/project/src/callbacks/DialogueCallbacks.ts @@ -90,7 +90,7 @@ export class DialogueCallbacks implements OnUpdate sessionID: string, ): IGetBodyResponseData { - return this.httpResponse.getBody(this.dialogueController.generateDialogueList(sessionID)); + return this.httpResponse.getBody(this.dialogueController.generateDialogueList(sessionID), 0, null, false); } /** Handle client/mail/dialog/view */ @@ -100,7 +100,7 @@ export class DialogueCallbacks implements OnUpdate sessionID: string, ): IGetBodyResponseData { - return this.httpResponse.getBody(this.dialogueController.generateDialogueView(info, sessionID)); + return this.httpResponse.getBody(this.dialogueController.generateDialogueView(info, sessionID), 0, null, false); } /** Handle client/mail/dialog/info */ diff --git a/project/src/controllers/DialogueController.ts b/project/src/controllers/DialogueController.ts index 095bf7c9..1407d3d6 100644 --- a/project/src/controllers/DialogueController.ts +++ b/project/src/controllers/DialogueController.ts @@ -1,46 +1,44 @@ -import { inject, injectable } from "tsyringe"; +import { inject, injectAll, injectable } from "tsyringe"; +import { IDialogueChatBot } from "@spt-aki/helpers/Dialogue/IDialogueChatBot"; import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper"; -import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper"; import { IGetAllAttachmentsResponse } from "@spt-aki/models/eft/dialog/IGetAllAttachmentsResponse"; import { IGetFriendListDataResponse } from "@spt-aki/models/eft/dialog/IGetFriendListDataResponse"; import { IGetMailDialogViewRequestData } from "@spt-aki/models/eft/dialog/IGetMailDialogViewRequestData"; import { IGetMailDialogViewResponseData } from "@spt-aki/models/eft/dialog/IGetMailDialogViewResponseData"; import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; import { Dialogue, DialogueInfo, IAkiProfile, IUserDialogInfo, Message } from "@spt-aki/models/eft/profile/IAkiProfile"; -import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; -import { GiftSentResult } from "@spt-aki/models/enums/GiftSentResult"; -import { MemberCategory } from "@spt-aki/models/enums/MemberCategory"; import { MessageType } from "@spt-aki/models/enums/MessageType"; -import { ICoreConfig } from "@spt-aki/models/spt/config/ICoreConfig"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; -import { ConfigServer } from "@spt-aki/servers/ConfigServer"; import { SaveServer } from "@spt-aki/servers/SaveServer"; -import { GiftService } from "@spt-aki/services/GiftService"; import { MailSendService } from "@spt-aki/services/MailSendService"; -import { HashUtil } from "@spt-aki/utils/HashUtil"; -import { RandomUtil } from "@spt-aki/utils/RandomUtil"; import { TimeUtil } from "@spt-aki/utils/TimeUtil"; @injectable() export class DialogueController { - protected coreConfig: ICoreConfig; + protected registeredDialogueChatBots: Map = new Map(); constructor( @inject("WinstonLogger") protected logger: ILogger, @inject("SaveServer") protected saveServer: SaveServer, @inject("TimeUtil") protected timeUtil: TimeUtil, @inject("DialogueHelper") protected dialogueHelper: DialogueHelper, - @inject("ProfileHelper") protected profileHelper: ProfileHelper, - @inject("RandomUtil") protected randomUtil: RandomUtil, @inject("MailSendService") protected mailSendService: MailSendService, - @inject("GiftService") protected giftService: GiftService, - @inject("HashUtil") protected hashUtil: HashUtil, - @inject("ConfigServer") protected configServer: ConfigServer, + @injectAll("DialogueChatBot") protected dialogueChatBots: IDialogueChatBot[], ) { - this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE); + for (const dialogueChatBot of dialogueChatBots) + { + if (this.registeredDialogueChatBots.has(dialogueChatBot.getChatBot()._id)) + { + this.logger.error( + `Could not register ${dialogueChatBot.getChatBot()._id} as it is already in use. Skipping.`, + ); + continue; + } + this.registeredDialogueChatBots.set(dialogueChatBot.getChatBot()._id, dialogueChatBot); + } } /** Handle onUpdate spt event */ @@ -61,7 +59,11 @@ export class DialogueController public getFriendList(sessionID: string): IGetFriendListDataResponse { // Force a fake friend called SPT into friend list - return { Friends: [this.getSptFriendData()], Ignore: [], InIgnoreList: [] }; + return { + Friends: Array.from(this.registeredDialogueChatBots.values()).map((v) => v.getChatBot()), + Ignore: [], + InIgnoreList: [], + }; } /** @@ -118,7 +120,8 @@ export class DialogueController // User to user messages are special in that they need the player to exist in them, add if they don't if ( - messageType === MessageType.USER_MESSAGE && !dialog.Users?.find((x) => x._id === profile.characters.pmc._id) + messageType === MessageType.USER_MESSAGE + && !dialog.Users?.find((x) => x._id === profile.characters.pmc.sessionId) ) { if (!dialog.Users) @@ -127,7 +130,7 @@ export class DialogueController } dialog.Users.push({ - _id: profile.characters.pmc._id, + _id: profile.characters.pmc.sessionId, info: { Level: profile.characters.pmc.Info.Level, Nickname: profile.characters.pmc.Info.Nickname, @@ -193,7 +196,12 @@ export class DialogueController if (request.type === MessageType.USER_MESSAGE) { profile.dialogues[request.dialogId].Users = []; - profile.dialogues[request.dialogId].Users.push(this.getSptFriendData(request.dialogId)); + if (this.registeredDialogueChatBots.has(request.dialogId)) + { + profile.dialogues[request.dialogId].Users.push( + this.registeredDialogueChatBots.get(request.dialogId).getChatBot(), + ); + } } } @@ -356,134 +364,14 @@ export class DialogueController { this.mailSendService.sendPlayerMessageToNpc(sessionId, request.dialogId, request.text); - // Handle when player types a keyword to sptFriend user - if (request.dialogId.includes("sptFriend")) + if (this.registeredDialogueChatBots.has(request.dialogId)) { - this.handleChatWithSPTFriend(sessionId, request); + return this.registeredDialogueChatBots.get(request.dialogId).handleMessage(sessionId, request); } return request.dialogId; } - /** - * Send responses back to player when they communicate with SPT friend on friends list - * @param sessionId Session Id - * @param request send message request - */ - protected handleChatWithSPTFriend(sessionId: string, request: ISendMessageRequest): void - { - const sender = this.profileHelper.getPmcProfile(sessionId); - - const sptFriendUser = this.getSptFriendData(); - - const giftSent = this.giftService.sendGiftToPlayer(sessionId, request.text); - - if (giftSent === GiftSentResult.SUCCESS) - { - this.mailSendService.sendUserMessageToPlayer( - sessionId, - sptFriendUser, - this.randomUtil.getArrayValue([ - "Hey! you got the right code!", - "A secret code, how exciting!", - "You found a gift code!", - ]), - ); - - return; - } - - if (giftSent === GiftSentResult.FAILED_GIFT_ALREADY_RECEIVED) - { - this.mailSendService.sendUserMessageToPlayer( - sessionId, - sptFriendUser, - this.randomUtil.getArrayValue(["Looks like you already used that code", "You already have that!!"]), - ); - - return; - } - - if (request.text.toLowerCase().includes("love you")) - { - this.mailSendService.sendUserMessageToPlayer( - sessionId, - sptFriendUser, - this.randomUtil.getArrayValue([ - "That's quite forward but i love you too in a purely chatbot-human way", - "I love you too buddy :3!", - "uwu", - `love you too ${sender?.Info?.Nickname}`, - ]), - ); - } - - if (request.text.toLowerCase() === "spt") - { - this.mailSendService.sendUserMessageToPlayer( - sessionId, - sptFriendUser, - this.randomUtil.getArrayValue(["Its me!!", "spt? i've heard of that project"]), - ); - } - - if (["hello", "hi", "sup", "yo", "hey"].includes(request.text.toLowerCase())) - { - this.mailSendService.sendUserMessageToPlayer( - sessionId, - sptFriendUser, - this.randomUtil.getArrayValue([ - "Howdy", - "Hi", - "Greetings", - "Hello", - "bonjor", - "Yo", - "Sup", - "Heyyyyy", - "Hey there", - `Hello ${sender?.Info?.Nickname}`, - ]), - ); - } - - if (request.text.toLowerCase() === "nikita") - { - this.mailSendService.sendUserMessageToPlayer( - sessionId, - sptFriendUser, - this.randomUtil.getArrayValue([ - "I know that guy!", - "Cool guy, he made EFT!", - "Legend", - "Remember when he said webel-webel-webel-webel, classic nikita moment", - ]), - ); - } - - if (request.text.toLowerCase() === "are you a bot") - { - this.mailSendService.sendUserMessageToPlayer( - sessionId, - sptFriendUser, - this.randomUtil.getArrayValue(["beep boop", "**sad boop**", "probably", "sometimes", "yeah lol"]), - ); - } - } - - protected getSptFriendData(friendId = "sptFriend"): IUserDialogInfo - { - return { - _id: friendId, - info: { - Level: 1, - MemberCategory: MemberCategory.DEVELOPER, - Nickname: this.coreConfig.sptFriendNickname, - Side: "Usec", - }, - }; - } - /** * Get messages from a specific dialog that have items not expired * @param sessionId Session id diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index b5ea9d6a..59585894 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -246,6 +246,10 @@ import { VFS } from "@spt-aki/utils/VFS"; import { Watermark, WatermarkLocale } from "@spt-aki/utils/Watermark"; import { WinstonMainLogger } from "@spt-aki/utils/logging/WinstonMainLogger"; import { WinstonRequestLogger } from "@spt-aki/utils/logging/WinstonRequestLogger"; +import {SptDialogueChatBot} from "@spt-aki/helpers/Dialogue/SptDialogueChatBot"; +import {CommandoDialogueChatBot} from "@spt-aki/helpers/Dialogue/CommandoDialogueChatBot"; +import {GiveSptCommand} from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveSptCommand"; +import {SptCommandoCommands} from "@spt-aki/helpers/Dialogue/Commando/SptCommandoCommands"; /** * Handle the registration of classes to be used by the Dependency Injection code @@ -357,6 +361,16 @@ export class Container depContainer.registerType("SaveLoadRouter", "InraidSaveLoadRouter"); depContainer.registerType("SaveLoadRouter", "InsuranceSaveLoadRouter"); depContainer.registerType("SaveLoadRouter", "ProfileSaveLoadRouter"); + + // Chat Bots + depContainer.registerType("DialogueChatBot", "SptDialogueChatBot"); + depContainer.registerType("DialogueChatBot", "CommandoDialogueChatBot"); + + // Commando Commands + depContainer.registerType("CommandoCommand", "SptCommandoCommands"); + + // SptCommando Commands + depContainer.registerType("SptCommand", "GiveSptCommand"); } private static registerUtils(depContainer: DependencyContainer): void @@ -566,6 +580,15 @@ export class Container }); depContainer.register("BotDifficultyHelper", { useClass: BotDifficultyHelper }); depContainer.register("RepeatableQuestHelper", { useClass: RepeatableQuestHelper }); + + // ChatBots + depContainer.register("SptDialogueChatBot", SptDialogueChatBot); + depContainer.register("CommandoDialogueChatBot", CommandoDialogueChatBot); + // SptCommando + depContainer.register("SptCommandoCommands", SptCommandoCommands); + // SptCommands + depContainer.register("GiveSptCommand", GiveSptCommand); + } private static registerLoaders(depContainer: DependencyContainer): void diff --git a/project/src/helpers/Dialogue/Commando/ICommandoAction.ts b/project/src/helpers/Dialogue/Commando/ICommandoAction.ts new file mode 100644 index 00000000..62cfaf01 --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/ICommandoAction.ts @@ -0,0 +1,7 @@ +import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; +import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; + +export interface ICommandoAction +{ + handle(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string; +} diff --git a/project/src/helpers/Dialogue/Commando/ICommandoCommand.ts b/project/src/helpers/Dialogue/Commando/ICommandoCommand.ts new file mode 100644 index 00000000..39338bb0 --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/ICommandoCommand.ts @@ -0,0 +1,9 @@ +import { ICommandoAction } from "@spt-aki/helpers/Dialogue/Commando/ICommandoAction"; + +export interface ICommandoCommand +{ + getCommandPrefix(): string; + getCommandHelp(command: string): string; + getCommands(): Set; + getCommandAction(command: string): ICommandoAction; +} diff --git a/project/src/helpers/Dialogue/Commando/SptCommandoCommands.ts b/project/src/helpers/Dialogue/Commando/SptCommandoCommands.ts new file mode 100644 index 00000000..a087880d --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/SptCommandoCommands.ts @@ -0,0 +1,38 @@ +import { ICommandoAction } from "@spt-aki/helpers/Dialogue/Commando/ICommandoAction"; +import { ICommandoCommand } from "@spt-aki/helpers/Dialogue/Commando/ICommandoCommand"; +import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand"; +import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; +import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; +import { injectAll, injectable } from "tsyringe"; + +@injectable() +export class SptCommandoCommands implements ICommandoCommand +{ + constructor( + @injectAll("SptCommand") protected sptCommands: ISptCommand[] + ) + { + } + + public getCommandHelp(command: string): string + { + return this.sptCommands.find(c => c.getCommand() === command)?.getCommandHelp(); + } + + public getCommandPrefix(): string + { + return "spt"; + } + + public getCommands(): Set + { + return new Set(this.sptCommands.map(c => c.getCommand())); + } + + public getCommandAction(command: string): ICommandoAction + { + return this.sptCommands.find(c => c.getCommand() === command); + } + + +} diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts new file mode 100644 index 00000000..e19f732e --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts @@ -0,0 +1,128 @@ +import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand"; +import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; +import { PresetHelper } from "@spt-aki/helpers/PresetHelper"; +import { Item } from "@spt-aki/models/eft/common/tables/IItem"; +import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; +import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; +import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; +import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; +import { MailSendService } from "@spt-aki/services/MailSendService"; +import { HashUtil } from "@spt-aki/utils/HashUtil"; +import { JsonUtil } from "@spt-aki/utils/JsonUtil"; +import { inject, injectable } from "tsyringe"; + +@injectable() +export class GiveSptCommand implements ISptCommand +{ + public constructor( + @inject("WinstonLogger") protected logger: ILogger, + @inject("ItemHelper") protected itemHelper: ItemHelper, + @inject("HashUtil") protected hashUtil: HashUtil, + @inject("JsonUtil") protected jsonUtil: JsonUtil, + @inject("PresetHelper") protected presetHelper: PresetHelper, + @inject("MailSendService") protected mailSendService: MailSendService, + ) + { + } + + public getCommand(): string + { + return "give"; + } + + public getCommandHelp(): string + { + return "Usage: spt give tplId quantity"; + } + + public handle(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string + { + const giveCommand = request.text.split(" "); + if (giveCommand[1] !== "give") + { + this.logger.error("Invalid action received for give command!"); + return request.dialogId; + } + + if (!giveCommand[2]) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + "Invalid use of give command! Template ID is missing. Use \"Help\" for more info", + ); + return request.dialogId; + } + const tplId = giveCommand[2]; + + if (!giveCommand[3]) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + "Invalid use of give command! Quantity is missing. Use \"Help\" for more info", + ); + return request.dialogId; + } + const quantity = giveCommand[3]; + + if (Number.isNaN(+quantity)) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + "Invalid use of give command! Quantity is not a valid integer. Use \"Help\" for more info", + ); + return request.dialogId; + } + + const checkedItem = this.itemHelper.getItem(tplId); + if (!checkedItem[0]) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + "Invalid template ID requested for give command. The item doesnt exists on the DB.", + ); + return request.dialogId; + } + + const itemsToSend: Item[] = []; + if (this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.WEAPON)) + { + const preset = this.presetHelper.getDefaultPreset(checkedItem[1]._id); + if (!preset) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + "Invalid weapon template ID requested. There are no default presets for this weapon.", + ); + return request.dialogId; + } + itemsToSend.push(...this.jsonUtil.clone(preset._items)); + } + else if (this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.AMMO_BOX)) + { + for (let i = 0; i < +quantity; i++) + { + const ammoBoxArray: Item[] = []; + ammoBoxArray.push({ _id: this.hashUtil.generate(), _tpl: checkedItem[1]._id }); + this.itemHelper.addCartridgesToAmmoBox(ammoBoxArray, checkedItem[1]); + itemsToSend.push(...ammoBoxArray); + } + } + else + { + const item: Item = { + _id: this.hashUtil.generate(), + _tpl: checkedItem[1]._id, + upd: { StackObjectsCount: +quantity }, + }; + itemsToSend.push(...this.itemHelper.splitStack(item)); + } + + this.mailSendService.sendSystemMessageToPlayer(sessionId, "Give command!", itemsToSend); + return request.dialogId; + } +} diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/ISptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/ISptCommand.ts new file mode 100644 index 00000000..b187b62e --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/SptCommands/ISptCommand.ts @@ -0,0 +1,7 @@ +import { ICommandoAction } from "@spt-aki/helpers/Dialogue/Commando/ICommandoAction"; + +export interface ISptCommand extends ICommandoAction +{ + getCommand(): string; + getCommandHelp(): string; +} diff --git a/project/src/helpers/Dialogue/CommandoDialogueChatBot.ts b/project/src/helpers/Dialogue/CommandoDialogueChatBot.ts new file mode 100644 index 00000000..8bbaff1d --- /dev/null +++ b/project/src/helpers/Dialogue/CommandoDialogueChatBot.ts @@ -0,0 +1,88 @@ +import { inject, injectAll, injectable } from "tsyringe"; + +import { ICommandoAction } from "@spt-aki/helpers/Dialogue/Commando/ICommandoAction"; +import { ICommandoCommand } from "@spt-aki/helpers/Dialogue/Commando/ICommandoCommand"; +import { IDialogueChatBot } from "@spt-aki/helpers/Dialogue/IDialogueChatBot"; +import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; +import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; +import { MemberCategory } from "@spt-aki/models/enums/MemberCategory"; +import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; +import { MailSendService } from "@spt-aki/services/MailSendService"; + +@injectable() +export class CommandoDialogueChatBot implements IDialogueChatBot +{ + + // A map that contains the command prefix. That contains a map that contains the prefix commands with their respective actions. + protected registeredCommands: Map> = new Map>(); + public constructor( + @inject("WinstonLogger") protected logger: ILogger, + @inject("MailSendService") protected mailSendService: MailSendService, + @injectAll("CommandoCommand") protected commandoCommands: ICommandoCommand[] + ) + { + for (const commandoCommand of commandoCommands) + { + if (this.registeredCommands.has(commandoCommand.getCommandPrefix()) || commandoCommand.getCommandPrefix().toLowerCase() === "help") + { + this.logger.error(`Could not registered command prefix ${commandoCommand.getCommandPrefix()} as it already has been registered. Skipping.`); + continue; + } + + const commandMap = new Map(); + this.registeredCommands.set(commandoCommand.getCommandPrefix(), commandMap); + for (const command of commandoCommand.getCommands()) + { + commandMap.set(command, commandoCommand.getCommandAction(command)) + } + } + } + + public getChatBot(): IUserDialogInfo + { + return { + _id: "sptCommando", + info: { + Level: 1, + MemberCategory: MemberCategory.DEVELOPER, + Nickname: "Commando", + Side: "Usec", + }, + }; + } + + public handleMessage(sessionId: string, request: ISendMessageRequest): string + { + if ((request.text ?? "").length === 0) + { + this.logger.error("Commando command came in as empty text! Invalid data!"); + return request.dialogId; + } + + const splitCommand = request.text.split(" "); + + if (this.registeredCommands.has(splitCommand[0]) && this.registeredCommands.get(splitCommand[0]).has(splitCommand[1])) + return this.registeredCommands.get(splitCommand[0]).get(splitCommand[1]).handle(this.getChatBot(), sessionId, request); + + if (splitCommand[0].toLowerCase() === "help") + { + const helpMessage = this.commandoCommands.filter(c => this.registeredCommands.has(c.getCommandPrefix())) + .filter(c => Array.from(c.getCommands()).some(com => this.registeredCommands.get(c.getCommandPrefix()).has(com))) + .map(c => `Help for ${c.getCommandPrefix()}:\n${Array.from(c.getCommands()).map(command => c.getCommandHelp(command)).join("\n")}`) + .join("\n"); + this.mailSendService.sendUserMessageToPlayer( + sessionId, + this.getChatBot(), + helpMessage + ); + return request.dialogId; + } + + this.mailSendService.sendUserMessageToPlayer( + sessionId, + this.getChatBot(), + `Im sorry soldier, I dont recognize the command you are trying to use! Type "help" to see available commands.` + ); + } + +} diff --git a/project/src/helpers/Dialogue/IDialogueChatBot.ts b/project/src/helpers/Dialogue/IDialogueChatBot.ts new file mode 100644 index 00000000..a39ffa8a --- /dev/null +++ b/project/src/helpers/Dialogue/IDialogueChatBot.ts @@ -0,0 +1,8 @@ +import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; +import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; + +export interface IDialogueChatBot +{ + getChatBot(): IUserDialogInfo; + handleMessage(sessionId: string, request: ISendMessageRequest): string; +} diff --git a/project/src/helpers/Dialogue/SptDialogueChatBot.ts b/project/src/helpers/Dialogue/SptDialogueChatBot.ts new file mode 100644 index 00000000..2e56b338 --- /dev/null +++ b/project/src/helpers/Dialogue/SptDialogueChatBot.ts @@ -0,0 +1,151 @@ +import { inject, injectable } from "tsyringe"; + +import { IDialogueChatBot } from "@spt-aki/helpers/Dialogue/IDialogueChatBot"; +import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper"; +import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; +import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; +import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; +import { GiftSentResult } from "@spt-aki/models/enums/GiftSentResult"; +import { MemberCategory } from "@spt-aki/models/enums/MemberCategory"; +import { ICoreConfig } from "@spt-aki/models/spt/config/ICoreConfig"; +import { ConfigServer } from "@spt-aki/servers/ConfigServer"; +import { GiftService } from "@spt-aki/services/GiftService"; +import { MailSendService } from "@spt-aki/services/MailSendService"; +import { RandomUtil } from "@spt-aki/utils/RandomUtil"; + +@injectable() +export class SptDialogueChatBot implements IDialogueChatBot +{ + protected coreConfig: ICoreConfig; + public constructor( + @inject("ProfileHelper") protected profileHelper: ProfileHelper, + @inject("RandomUtil") protected randomUtil: RandomUtil, + @inject("MailSendService") protected mailSendService: MailSendService, + @inject("GiftService") protected giftService: GiftService, + @inject("ConfigServer") protected configServer: ConfigServer + ) + { + this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE); + } + + public getChatBot(): IUserDialogInfo + { + return { + _id: "sptFriend", + info: { + Level: 1, + MemberCategory: MemberCategory.DEVELOPER, + Nickname: this.coreConfig.sptFriendNickname, + Side: "Usec", + }, + }; + } + + /** + * Send responses back to player when they communicate with SPT friend on friends list + * @param sessionId Session Id + * @param request send message request + */ + public handleMessage(sessionId: string, request: ISendMessageRequest): string + { + const sender = this.profileHelper.getPmcProfile(sessionId); + + const sptFriendUser = this.getChatBot(); + + const giftSent = this.giftService.sendGiftToPlayer(sessionId, request.text); + + if (giftSent === GiftSentResult.SUCCESS) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + sptFriendUser, + this.randomUtil.getArrayValue([ + "Hey! you got the right code!", + "A secret code, how exciting!", + "You found a gift code!", + ]) + ); + + return; + } + + if (giftSent === GiftSentResult.FAILED_GIFT_ALREADY_RECEIVED) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + sptFriendUser, + this.randomUtil.getArrayValue(["Looks like you already used that code", "You already have that!!"]) + ); + + return; + } + + if (request.text.toLowerCase().includes("love you")) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + sptFriendUser, + this.randomUtil.getArrayValue([ + "That's quite forward but i love you too in a purely chatbot-human way", + "I love you too buddy :3!", + "uwu", + `love you too ${sender?.Info?.Nickname}`, + ]) + ); + } + + if (request.text.toLowerCase() === "spt") + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + sptFriendUser, + this.randomUtil.getArrayValue(["Its me!!", "spt? i've heard of that project"]) + ); + } + + if (["hello", "hi", "sup", "yo", "hey"].includes(request.text.toLowerCase())) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + sptFriendUser, + this.randomUtil.getArrayValue([ + "Howdy", + "Hi", + "Greetings", + "Hello", + "bonjor", + "Yo", + "Sup", + "Heyyyyy", + "Hey there", + `Hello ${sender?.Info?.Nickname}`, + ]) + ); + } + + if (request.text.toLowerCase() === "nikita") + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + sptFriendUser, + this.randomUtil.getArrayValue([ + "I know that guy!", + "Cool guy, he made EFT!", + "Legend", + "Remember when he said webel-webel-webel-webel, classic nikita moment", + ]) + ); + } + + if (request.text.toLowerCase() === "are you a bot") + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + sptFriendUser, + this.randomUtil.getArrayValue(["beep boop", "**sad boop**", "probably", "sometimes", "yeah lol"]) + ); + } + + return request.dialogId; + } +} diff --git a/project/src/utils/HttpResponseUtil.ts b/project/src/utils/HttpResponseUtil.ts index e543c2ae..307d8949 100644 --- a/project/src/utils/HttpResponseUtil.ts +++ b/project/src/utils/HttpResponseUtil.ts @@ -36,14 +36,16 @@ export class HttpResponseUtil /** * Game client needs server responses in a particular format - * @param data - * @param err - * @param errmsg - * @returns + * @param data + * @param err + * @param errmsg + * @returns */ - public getBody(data: T, err = 0, errmsg = null): IGetBodyResponseData + public getBody(data: T, err = 0, errmsg = null, sanitize = true): IGetBodyResponseData { - return this.clearString(this.getUnclearedBody(data, err, errmsg)); + return sanitize + ? this.clearString(this.getUnclearedBody(data, err, errmsg)) + : (this.getUnclearedBody(data, err, errmsg) as any); } public getUnclearedBody(data: any, err = 0, errmsg = null): string From 0ade8f4b9c4420395b08a34cca46de50a074733b Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 25 Dec 2023 08:38:42 +0000 Subject: [PATCH 05/10] Added configs and simplification to ChatBot (!180) Co-authored-by: clodan Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/180 Co-authored-by: Alex Co-committed-by: Alex --- project/assets/configs/core.json | 9 ++- project/src/controllers/DialogueController.ts | 59 ++++++++++-------- project/src/di/Container.ts | 21 ++++--- .../Dialogue/Commando/ICommandoAction.ts | 7 --- .../Dialogue/Commando/ICommandoCommand.ts | 5 +- .../Dialogue/Commando/SptCommandoCommands.ts | 47 ++++++++++++--- .../Commando/SptCommands/GiveSptCommand.ts | 2 +- .../Commando/SptCommands/ISptCommand.ts | 6 +- .../Dialogue/CommandoDialogueChatBot.ts | 60 +++++++------------ project/src/models/spt/config/ICoreConfig.ts | 13 ++++ 10 files changed, 136 insertions(+), 93 deletions(-) delete mode 100644 project/src/helpers/Dialogue/Commando/ICommandoAction.ts diff --git a/project/assets/configs/core.json b/project/assets/configs/core.json index 4f4f8bd9..56b94fcc 100644 --- a/project/assets/configs/core.json +++ b/project/assets/configs/core.json @@ -12,6 +12,13 @@ }, "features": { "autoInstallModDependencies": false, - "compressProfile": false + "compressProfile": false, + "chatbotFeatures": { + "sptFriendEnabled": true, + "commandoEnabled": true, + "commandoFeatures": { + "giveCommandEnabled": false + } + } } } diff --git a/project/src/controllers/DialogueController.ts b/project/src/controllers/DialogueController.ts index 1407d3d6..51125bcb 100644 --- a/project/src/controllers/DialogueController.ts +++ b/project/src/controllers/DialogueController.ts @@ -8,8 +8,11 @@ import { IGetMailDialogViewRequestData } from "@spt-aki/models/eft/dialog/IGetMa import { IGetMailDialogViewResponseData } from "@spt-aki/models/eft/dialog/IGetMailDialogViewResponseData"; import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; import { Dialogue, DialogueInfo, IAkiProfile, IUserDialogInfo, Message } from "@spt-aki/models/eft/profile/IAkiProfile"; +import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { MessageType } from "@spt-aki/models/enums/MessageType"; +import { ICoreConfig } from "@spt-aki/models/spt/config/ICoreConfig"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; +import { ConfigServer } from "@spt-aki/servers/ConfigServer"; import { SaveServer } from "@spt-aki/servers/SaveServer"; import { MailSendService } from "@spt-aki/services/MailSendService"; import { TimeUtil } from "@spt-aki/utils/TimeUtil"; @@ -17,28 +20,39 @@ import { TimeUtil } from "@spt-aki/utils/TimeUtil"; @injectable() export class DialogueController { - protected registeredDialogueChatBots: Map = new Map(); - 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("ConfigServer") protected configServer: ConfigServer, @injectAll("DialogueChatBot") protected dialogueChatBots: IDialogueChatBot[], ) { - for (const dialogueChatBot of dialogueChatBots) + const coreConfigs = this.configServer.getConfig(ConfigTypes.CORE); + // if give command is disabled or commando commands are disabled + if (!coreConfigs.features?.chatbotFeatures?.commandoEnabled) { - if (this.registeredDialogueChatBots.has(dialogueChatBot.getChatBot()._id)) - { - this.logger.error( - `Could not register ${dialogueChatBot.getChatBot()._id} as it is already in use. Skipping.`, - ); - continue; - } - this.registeredDialogueChatBots.set(dialogueChatBot.getChatBot()._id, dialogueChatBot); + const sptCommando = this.dialogueChatBots.find((c) => + c.getChatBot()._id.toLocaleLowerCase() === "sptcommando" + ); + this.dialogueChatBots.splice(this.dialogueChatBots.indexOf(sptCommando), 1); } + if (!coreConfigs.features?.chatbotFeatures?.sptFriendEnabled) + { + const sptFriend = this.dialogueChatBots.find((c) => c.getChatBot()._id.toLocaleLowerCase() === "sptFriend"); + this.dialogueChatBots.splice(this.dialogueChatBots.indexOf(sptFriend), 1); + } + } + + public registerChatBot(chatBot: IDialogueChatBot): void + { + if (this.dialogueChatBots.some((cb) => cb.getChatBot()._id === chatBot.getChatBot()._id)) + { + throw new Error(`The chat bot ${chatBot.getChatBot()._id} being registered already exists!`); + } + this.dialogueChatBots.push(chatBot); } /** Handle onUpdate spt event */ @@ -59,11 +73,7 @@ export class DialogueController public getFriendList(sessionID: string): IGetFriendListDataResponse { // Force a fake friend called SPT into friend list - return { - Friends: Array.from(this.registeredDialogueChatBots.values()).map((v) => v.getChatBot()), - Ignore: [], - InIgnoreList: [], - }; + return { Friends: this.dialogueChatBots.map((v) => v.getChatBot()), Ignore: [], InIgnoreList: [] }; } /** @@ -196,11 +206,10 @@ export class DialogueController if (request.type === MessageType.USER_MESSAGE) { profile.dialogues[request.dialogId].Users = []; - if (this.registeredDialogueChatBots.has(request.dialogId)) + const chatBot = this.dialogueChatBots.find((cb) => cb.getChatBot()._id === request.dialogId); + if (chatBot) { - profile.dialogues[request.dialogId].Users.push( - this.registeredDialogueChatBots.get(request.dialogId).getChatBot(), - ); + profile.dialogues[request.dialogId].Users.push(chatBot.getChatBot()); } } } @@ -364,12 +373,10 @@ export class DialogueController { this.mailSendService.sendPlayerMessageToNpc(sessionId, request.dialogId, request.text); - if (this.registeredDialogueChatBots.has(request.dialogId)) - { - return this.registeredDialogueChatBots.get(request.dialogId).handleMessage(sessionId, request); - } - - return request.dialogId; + return this.dialogueChatBots.find((cb) => cb.getChatBot()._id === request.dialogId)?.handleMessage( + sessionId, + request, + ) ?? request.dialogId; } /** diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index 59585894..990d1d39 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -86,6 +86,10 @@ import { BotGeneratorHelper } from "@spt-aki/helpers/BotGeneratorHelper"; import { BotHelper } from "@spt-aki/helpers/BotHelper"; import { BotWeaponGeneratorHelper } from "@spt-aki/helpers/BotWeaponGeneratorHelper"; import { ContainerHelper } from "@spt-aki/helpers/ContainerHelper"; +import { SptCommandoCommands } from "@spt-aki/helpers/Dialogue/Commando/SptCommandoCommands"; +import { GiveSptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveSptCommand"; +import { CommandoDialogueChatBot } from "@spt-aki/helpers/Dialogue/CommandoDialogueChatBot"; +import { SptDialogueChatBot } from "@spt-aki/helpers/Dialogue/SptDialogueChatBot"; import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper"; import { DurabilityLimitsHelper } from "@spt-aki/helpers/DurabilityLimitsHelper"; import { GameEventHelper } from "@spt-aki/helpers/GameEventHelper"; @@ -246,10 +250,6 @@ import { VFS } from "@spt-aki/utils/VFS"; import { Watermark, WatermarkLocale } from "@spt-aki/utils/Watermark"; import { WinstonMainLogger } from "@spt-aki/utils/logging/WinstonMainLogger"; import { WinstonRequestLogger } from "@spt-aki/utils/logging/WinstonRequestLogger"; -import {SptDialogueChatBot} from "@spt-aki/helpers/Dialogue/SptDialogueChatBot"; -import {CommandoDialogueChatBot} from "@spt-aki/helpers/Dialogue/CommandoDialogueChatBot"; -import {GiveSptCommand} from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveSptCommand"; -import {SptCommandoCommands} from "@spt-aki/helpers/Dialogue/Commando/SptCommandoCommands"; /** * Handle the registration of classes to be used by the Dependency Injection code @@ -583,12 +583,15 @@ export class Container // ChatBots depContainer.register("SptDialogueChatBot", SptDialogueChatBot); - depContainer.register("CommandoDialogueChatBot", CommandoDialogueChatBot); + depContainer.register("CommandoDialogueChatBot", CommandoDialogueChatBot, { + lifecycle: Lifecycle.Singleton, + }); // SptCommando - depContainer.register("SptCommandoCommands", SptCommandoCommands); + depContainer.register("SptCommandoCommands", SptCommandoCommands, { + lifecycle: Lifecycle.Singleton, + }); // SptCommands depContainer.register("GiveSptCommand", GiveSptCommand); - } private static registerLoaders(depContainer: DependencyContainer): void @@ -753,7 +756,9 @@ export class Container depContainer.register("CustomizationController", { useClass: CustomizationController, }); - depContainer.register("DialogueController", { useClass: DialogueController }); + depContainer.register("DialogueController", { useClass: DialogueController }, { + lifecycle: Lifecycle.Singleton, + }); depContainer.register("GameController", { useClass: GameController }); depContainer.register("HandbookController", { useClass: HandbookController }); depContainer.register("HealthController", { useClass: HealthController }); diff --git a/project/src/helpers/Dialogue/Commando/ICommandoAction.ts b/project/src/helpers/Dialogue/Commando/ICommandoAction.ts deleted file mode 100644 index 62cfaf01..00000000 --- a/project/src/helpers/Dialogue/Commando/ICommandoAction.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; -import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; - -export interface ICommandoAction -{ - handle(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string; -} diff --git a/project/src/helpers/Dialogue/Commando/ICommandoCommand.ts b/project/src/helpers/Dialogue/Commando/ICommandoCommand.ts index 39338bb0..03083f3b 100644 --- a/project/src/helpers/Dialogue/Commando/ICommandoCommand.ts +++ b/project/src/helpers/Dialogue/Commando/ICommandoCommand.ts @@ -1,9 +1,10 @@ -import { ICommandoAction } from "@spt-aki/helpers/Dialogue/Commando/ICommandoAction"; +import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; +import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; export interface ICommandoCommand { getCommandPrefix(): string; getCommandHelp(command: string): string; getCommands(): Set; - getCommandAction(command: string): ICommandoAction; + handle(command: string, commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string; } diff --git a/project/src/helpers/Dialogue/Commando/SptCommandoCommands.ts b/project/src/helpers/Dialogue/Commando/SptCommandoCommands.ts index a087880d..82017853 100644 --- a/project/src/helpers/Dialogue/Commando/SptCommandoCommands.ts +++ b/project/src/helpers/Dialogue/Commando/SptCommandoCommands.ts @@ -1,22 +1,44 @@ -import { ICommandoAction } from "@spt-aki/helpers/Dialogue/Commando/ICommandoAction"; import { ICommandoCommand } from "@spt-aki/helpers/Dialogue/Commando/ICommandoCommand"; import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand"; import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; -import { injectAll, injectable } from "tsyringe"; +import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; +import { ICoreConfig } from "@spt-aki/models/spt/config/ICoreConfig"; +import { ConfigServer } from "@spt-aki/servers/ConfigServer"; +import { inject, injectAll, injectable } from "tsyringe"; @injectable() export class SptCommandoCommands implements ICommandoCommand { constructor( - @injectAll("SptCommand") protected sptCommands: ISptCommand[] + @inject("ConfigServer") protected configServer: ConfigServer, + @injectAll("SptCommand") protected sptCommands: ISptCommand[], ) { + const coreConfigs = this.configServer.getConfig(ConfigTypes.CORE); + // if give command is disabled or commando commands are disabled + if ( + !(coreConfigs.features?.chatbotFeatures?.commandoFeatures?.giveCommandEnabled + && coreConfigs.features?.chatbotFeatures?.commandoEnabled) + ) + { + const giveCommand = this.sptCommands.find((c) => c.getCommand().toLocaleLowerCase() === "give"); + this.sptCommands.splice(this.sptCommands.indexOf(giveCommand), 1); + } + } + + public registerSptCommandoCommand(command: ISptCommand): void + { + if (this.sptCommands.some((c) => c.getCommand() === command.getCommand())) + { + throw new Error(`The command ${command.getCommand()} being registered for SPT Commands already exists!`); + } + this.sptCommands.push(command); } public getCommandHelp(command: string): string { - return this.sptCommands.find(c => c.getCommand() === command)?.getCommandHelp(); + return this.sptCommands.find((c) => c.getCommand() === command)?.getCommandHelp(); } public getCommandPrefix(): string @@ -26,13 +48,20 @@ export class SptCommandoCommands implements ICommandoCommand public getCommands(): Set { - return new Set(this.sptCommands.map(c => c.getCommand())); + return new Set(this.sptCommands.map((c) => c.getCommand())); } - public getCommandAction(command: string): ICommandoAction + public handle( + command: string, + commandHandler: IUserDialogInfo, + sessionId: string, + request: ISendMessageRequest, + ): string { - return this.sptCommands.find(c => c.getCommand() === command); + return this.sptCommands.find((c) => c.getCommand() === command).performAction( + commandHandler, + sessionId, + request, + ); } - - } diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts index e19f732e..a9731beb 100644 --- a/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts +++ b/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts @@ -35,7 +35,7 @@ export class GiveSptCommand implements ISptCommand return "Usage: spt give tplId quantity"; } - public handle(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string + public performAction(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string { const giveCommand = request.text.split(" "); if (giveCommand[1] !== "give") diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/ISptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/ISptCommand.ts index b187b62e..8aedd1ab 100644 --- a/project/src/helpers/Dialogue/Commando/SptCommands/ISptCommand.ts +++ b/project/src/helpers/Dialogue/Commando/SptCommands/ISptCommand.ts @@ -1,7 +1,9 @@ -import { ICommandoAction } from "@spt-aki/helpers/Dialogue/Commando/ICommandoAction"; +import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; +import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; -export interface ISptCommand extends ICommandoAction +export interface ISptCommand { getCommand(): string; getCommandHelp(): string; + performAction(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string; } diff --git a/project/src/helpers/Dialogue/CommandoDialogueChatBot.ts b/project/src/helpers/Dialogue/CommandoDialogueChatBot.ts index 8bbaff1d..e73cda1c 100644 --- a/project/src/helpers/Dialogue/CommandoDialogueChatBot.ts +++ b/project/src/helpers/Dialogue/CommandoDialogueChatBot.ts @@ -1,6 +1,5 @@ import { inject, injectAll, injectable } from "tsyringe"; -import { ICommandoAction } from "@spt-aki/helpers/Dialogue/Commando/ICommandoAction"; import { ICommandoCommand } from "@spt-aki/helpers/Dialogue/Commando/ICommandoCommand"; import { IDialogueChatBot } from "@spt-aki/helpers/Dialogue/IDialogueChatBot"; import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; @@ -12,42 +11,30 @@ import { MailSendService } from "@spt-aki/services/MailSendService"; @injectable() export class CommandoDialogueChatBot implements IDialogueChatBot { - - // A map that contains the command prefix. That contains a map that contains the prefix commands with their respective actions. - protected registeredCommands: Map> = new Map>(); public constructor( @inject("WinstonLogger") protected logger: ILogger, @inject("MailSendService") protected mailSendService: MailSendService, - @injectAll("CommandoCommand") protected commandoCommands: ICommandoCommand[] + @injectAll("CommandoCommand") protected commandoCommands: ICommandoCommand[], ) { - for (const commandoCommand of commandoCommands) - { - if (this.registeredCommands.has(commandoCommand.getCommandPrefix()) || commandoCommand.getCommandPrefix().toLowerCase() === "help") - { - this.logger.error(`Could not registered command prefix ${commandoCommand.getCommandPrefix()} as it already has been registered. Skipping.`); - continue; - } + } - const commandMap = new Map(); - this.registeredCommands.set(commandoCommand.getCommandPrefix(), commandMap); - for (const command of commandoCommand.getCommands()) - { - commandMap.set(command, commandoCommand.getCommandAction(command)) - } + public registerCommandoCommand(commandoCommand: ICommandoCommand): void + { + if (this.commandoCommands.some((cc) => cc.getCommandPrefix() === commandoCommand.getCommandPrefix())) + { + throw new Error( + `The commando command ${commandoCommand.getCommandPrefix()} being registered already exists!`, + ); } + this.commandoCommands.push(commandoCommand); } public getChatBot(): IUserDialogInfo { return { _id: "sptCommando", - info: { - Level: 1, - MemberCategory: MemberCategory.DEVELOPER, - Nickname: "Commando", - Side: "Usec", - }, + info: { Level: 1, MemberCategory: MemberCategory.DEVELOPER, Nickname: "Commando", Side: "Usec" }, }; } @@ -61,28 +48,27 @@ export class CommandoDialogueChatBot implements IDialogueChatBot const splitCommand = request.text.split(" "); - if (this.registeredCommands.has(splitCommand[0]) && this.registeredCommands.get(splitCommand[0]).has(splitCommand[1])) - return this.registeredCommands.get(splitCommand[0]).get(splitCommand[1]).handle(this.getChatBot(), sessionId, request); + const commandos = this.commandoCommands.filter((c) => c.getCommandPrefix() === splitCommand[0]); + if (commandos[0]?.getCommands().has(splitCommand[1])) + { + return commandos[0].handle(splitCommand[1], this.getChatBot(), sessionId, request); + } if (splitCommand[0].toLowerCase() === "help") { - const helpMessage = this.commandoCommands.filter(c => this.registeredCommands.has(c.getCommandPrefix())) - .filter(c => Array.from(c.getCommands()).some(com => this.registeredCommands.get(c.getCommandPrefix()).has(com))) - .map(c => `Help for ${c.getCommandPrefix()}:\n${Array.from(c.getCommands()).map(command => c.getCommandHelp(command)).join("\n")}`) - .join("\n"); - this.mailSendService.sendUserMessageToPlayer( - sessionId, - this.getChatBot(), - helpMessage - ); + const helpMessage = this.commandoCommands.map((c) => + `Help for ${c.getCommandPrefix()}:\n${ + Array.from(c.getCommands()).map((command) => c.getCommandHelp(command)).join("\n") + }` + ).join("\n"); + this.mailSendService.sendUserMessageToPlayer(sessionId, this.getChatBot(), helpMessage); return request.dialogId; } this.mailSendService.sendUserMessageToPlayer( sessionId, this.getChatBot(), - `Im sorry soldier, I dont recognize the command you are trying to use! Type "help" to see available commands.` + `Im sorry soldier, I dont recognize the command you are trying to use! Type "help" to see available commands.`, ); } - } diff --git a/project/src/models/spt/config/ICoreConfig.ts b/project/src/models/spt/config/ICoreConfig.ts index 13cebaec..e5ef1e81 100644 --- a/project/src/models/spt/config/ICoreConfig.ts +++ b/project/src/models/spt/config/ICoreConfig.ts @@ -32,4 +32,17 @@ export interface IServerFeatures /* Controls whether or not the server attempts to download mod dependencies not included in the server's executable */ autoInstallModDependencies: boolean; compressProfile: boolean; + chatbotFeatures: IChatbotFeatures; +} + +export interface IChatbotFeatures +{ + sptFriendEnabled: boolean; + commandoEnabled: boolean; + commandoFeatures: ICommandoFeatures; +} + +export interface ICommandoFeatures +{ + giveCommandEnabled: boolean; } From 6caf97d7ee27491b98a9ca60a75b618d9dc3fe4b Mon Sep 17 00:00:00 2001 From: Dev Date: Mon, 25 Dec 2023 13:22:31 +0000 Subject: [PATCH 06/10] Flag items given as FiR Fix weapon preset reqeust not returning more than 1 item --- .../Commando/SptCommands/GiveSptCommand.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts index a9731beb..6c4cf12c 100644 --- a/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts +++ b/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts @@ -82,7 +82,7 @@ export class GiveSptCommand implements ISptCommand this.mailSendService.sendUserMessageToPlayer( sessionId, commandHandler, - "Invalid template ID requested for give command. The item doesnt exists on the DB.", + "Invalid template ID requested for give command. The item doesn't exist in the DB.", ); return request.dialogId; } @@ -100,7 +100,14 @@ export class GiveSptCommand implements ISptCommand ); return request.dialogId; } - itemsToSend.push(...this.jsonUtil.clone(preset._items)); + + for (let i = 0; i < +quantity; i++) + { + // Make sure IDs are unique before adding to array - prevent collisions + const presetToSend = this.itemHelper.replaceIDs(null, this.jsonUtil.clone(preset._items)); + itemsToSend.push(... presetToSend); + } + } else if (this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.AMMO_BOX)) { @@ -117,7 +124,10 @@ export class GiveSptCommand implements ISptCommand const item: Item = { _id: this.hashUtil.generate(), _tpl: checkedItem[1]._id, - upd: { StackObjectsCount: +quantity }, + upd: { + StackObjectsCount: +quantity, + SpawnedInSession: true + }, }; itemsToSend.push(...this.itemHelper.splitStack(item)); } From 51895033c3e462e05e8b5ce86e71b63147749896 Mon Sep 17 00:00:00 2001 From: Dev Date: Tue, 26 Dec 2023 13:09:38 +0000 Subject: [PATCH 07/10] Disable daily scav pickup quests as they're broken, needs investigation and fixing before introducing them again --- project/assets/configs/quest.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/project/assets/configs/quest.json b/project/assets/configs/quest.json index e365ca38..64f12c1a 100644 --- a/project/assets/configs/quest.json +++ b/project/assets/configs/quest.json @@ -1552,8 +1552,7 @@ "types": [ "Exploration", "Elimination", - "Completion", - "Pickup" + "Completion" ], "resetTime": 86400, "numQuests": 1, From 538a5f280757e586ab148c054523e0603dcc98bd Mon Sep 17 00:00:00 2001 From: Dev Date: Tue, 26 Dec 2023 15:55:50 +0000 Subject: [PATCH 08/10] Fix Czech locale not being picked up correctly --- project/src/services/LocaleService.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/project/src/services/LocaleService.ts b/project/src/services/LocaleService.ts index c9bc8263..bf5b3fd8 100644 --- a/project/src/services/LocaleService.ts +++ b/project/src/services/LocaleService.ts @@ -36,7 +36,7 @@ export class LocaleService } this.logger.warning( - `Unable to find desired locale file using locale ${this.getDesiredGameLocale()} from config/locale.json, falling back to 'en'`, + `Unable to find desired locale file using locale: ${this.getDesiredGameLocale()} from config/locale.json, falling back to 'en'`, ); return this.databaseServer.getTables().locales.global.en; @@ -103,6 +103,12 @@ export class LocaleService return "en"; } + // BSG map Czech to CZ for some reason + if (platformLocale.language === "cs") + { + return "cz"; + } + return platformLocale.language; } } From f9a1d117ea7dcd86eb50a740cd2e4b418c0d5b2b Mon Sep 17 00:00:00 2001 From: Dev Date: Tue, 26 Dec 2023 16:26:15 +0000 Subject: [PATCH 09/10] prevent `client/quest/list` handler from showing quests from traders who no longer exist --- project/src/controllers/QuestController.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/project/src/controllers/QuestController.ts b/project/src/controllers/QuestController.ts index b834b279..2f6b2083 100644 --- a/project/src/controllers/QuestController.ts +++ b/project/src/controllers/QuestController.ts @@ -104,6 +104,15 @@ export class QuestController continue; } + // Player can use trader mods then remove them, leaving quests behind + const trader = profile.TradersInfo[quest.traderId]; + if (!trader) + { + this.logger.debug(`Unable to show quest: ${quest.QuestName} as its for a trader: ${quest.traderId} that no longer exists.`); + + continue; + } + const questRequirements = this.questConditionHelper.getQuestConditions(quest.conditions.AvailableForStart); const loyaltyRequirements = this.questConditionHelper.getLoyaltyConditions( quest.conditions.AvailableForStart, From 40b999d04c68f1f52ab152d163c086a1c50f489b Mon Sep 17 00:00:00 2001 From: Dev Date: Wed, 27 Dec 2023 10:56:24 +0000 Subject: [PATCH 10/10] Update version --- project/assets/configs/core.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/assets/configs/core.json b/project/assets/configs/core.json index 56b94fcc..5518b071 100644 --- a/project/assets/configs/core.json +++ b/project/assets/configs/core.json @@ -1,5 +1,5 @@ { - "akiVersion": "3.7.5", + "akiVersion": "3.7.6", "projectName": "SPT-AKI", "compatibleTarkovVersion": "0.13.5.26535", "serverName": "SPT Server",