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/assets/configs/core.json b/project/assets/configs/core.json index 2755396e..5518b071 100644 --- a/project/assets/configs/core.json +++ b/project/assets/configs/core.json @@ -1,7 +1,7 @@ { - "akiVersion": "3.8.0", + "akiVersion": "3.7.6", "projectName": "SPT-AKI", - "compatibleTarkovVersion": "0.13.9.27050", + "compatibleTarkovVersion": "0.13.5.26535", "serverName": "SPT Server", "profileSaveIntervalSeconds": 15, "sptFriendNickname": "SPT", @@ -11,6 +11,14 @@ "fixProfileBreakingInventoryItemIssues": false }, "features": { - "autoInstallModDependencies": false + "autoInstallModDependencies": false, + "compressProfile": false, + "chatbotFeatures": { + "sptFriendEnabled": true, + "commandoEnabled": true, + "commandoFeatures": { + "giveCommandEnabled": false + } + } } } 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, 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 1c74a1c3..51125bcb 100644 --- a/project/src/controllers/DialogueController.ts +++ b/project/src/controllers/DialogueController.ts @@ -1,7 +1,7 @@ -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"; @@ -9,38 +9,50 @@ import { IGetMailDialogViewResponseData } from "@spt-aki/models/eft/dialog/IGetM 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; - 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); + const coreConfigs = this.configServer.getConfig(ConfigTypes.CORE); + // if give command is disabled or commando commands are disabled + if (!coreConfigs.features?.chatbotFeatures?.commandoEnabled) + { + 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 */ @@ -57,10 +69,11 @@ export class DialogueController * Handle client/friend/list * @returns IGetFriendListDataResponse */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars public getFriendList(sessionID: string): IGetFriendListDataResponse { // Force a fake friend called SPT into friend list - return { Friends: [this.getSptFriendData()], Ignore: [], InIgnoreList: [] }; + return { Friends: this.dialogueChatBots.map((v) => v.getChatBot()), Ignore: [], InIgnoreList: [] }; } /** @@ -117,7 +130,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) @@ -126,7 +140,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, @@ -141,7 +155,7 @@ export class DialogueController /** * Handle client/mail/dialog/view - * Handle player clicking 'messenger' and seeing all the messages they've received + * Handle player clicking 'messenger' and seeing all the messages they've recieved * Set the content of the dialogue on the details panel, showing all the messages * for the specified dialogue. * @param request Get dialog request @@ -173,7 +187,7 @@ export class DialogueController /** * Get dialog from player profile, create if doesn't exist * @param profile Player profile - * @param request get dialog request (params used when dialog doesn't exist in profile) + * @param request get dialog request (params used when dialog doesnt exist in profile) * @returns Dialogue */ protected getDialogByIdFromProfile(profile: IAkiProfile, request: IGetMailDialogViewRequestData): Dialogue @@ -192,7 +206,11 @@ export class DialogueController if (request.type === MessageType.USER_MESSAGE) { profile.dialogues[request.dialogId].Users = []; - profile.dialogues[request.dialogId].Users.push(this.getSptFriendData(request.dialogId)); + const chatBot = this.dialogueChatBots.find((cb) => cb.getChatBot()._id === request.dialogId); + if (chatBot) + { + profile.dialogues[request.dialogId].Users.push(chatBot.getChatBot()); + } } } @@ -211,7 +229,7 @@ export class DialogueController { result.push(...dialogUsers); - // Player doesn't exist, add them in before returning + // Player doesnt exist, add them in before returning if (!result.find((x) => x._id === fullProfile.info.id)) { const pmcProfile = fullProfile.characters.pmc; @@ -274,6 +292,7 @@ export class DialogueController if (!dialog) { this.logger.error(`No dialog in profile: ${sessionId} found with id: ${dialogueId}`); + return; } @@ -287,6 +306,7 @@ export class DialogueController if (!dialog) { this.logger.error(`No dialog in profile: ${sessionId} found with id: ${dialogueId}`); + return; } @@ -305,6 +325,7 @@ export class DialogueController if (!dialogs) { this.logger.error(`No dialog object in profile: ${sessionId}`); + return; } @@ -329,6 +350,7 @@ export class DialogueController if (!dialog) { this.logger.error(`No dialog in profile: ${sessionId} found with id: ${dialogueId}`); + return; } @@ -346,132 +368,15 @@ export class DialogueController } /** client/mail/msg/send */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars public sendMessage(sessionId: string, request: ISendMessageRequest): string { this.mailSendService.sendPlayerMessageToNpc(sessionId, request.dialogId, request.text); - // Handle when player types a keyword to sptFriend user - if (request.dialogId.includes("sptFriend")) - { - this.handleChatWithSPTFriend(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", - "Bonjour", - "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", - }, - }; + return this.dialogueChatBots.find((cb) => cb.getChatBot()._id === request.dialogId)?.handleMessage( + sessionId, + request, + ) ?? request.dialogId; } /** diff --git a/project/src/controllers/GameController.ts b/project/src/controllers/GameController.ts index 888aaca0..85d1558f 100644 --- a/project/src/controllers/GameController.ts +++ b/project/src/controllers/GameController.ts @@ -424,6 +424,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, @@ -443,7 +444,7 @@ export class GameController }, useProtobuf: false, utc_time: new Date().getTime() / 1000, - totalInGame: profile.Stats?.Eft?.TotalInGameTime ?? 0, + totalInGame: gameTime, }; return config; 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, diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index 6dca9ebe..a524d8a0 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"; @@ -355,6 +359,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 @@ -563,6 +577,18 @@ export class Container }); depContainer.register("BotDifficultyHelper", { useClass: BotDifficultyHelper }); depContainer.register("RepeatableQuestHelper", { useClass: RepeatableQuestHelper }); + + // ChatBots + depContainer.register("SptDialogueChatBot", SptDialogueChatBot); + depContainer.register("CommandoDialogueChatBot", CommandoDialogueChatBot, { + lifecycle: Lifecycle.Singleton, + }); + // SptCommando + depContainer.register("SptCommandoCommands", SptCommandoCommands, { + lifecycle: Lifecycle.Singleton, + }); + // SptCommands + depContainer.register("GiveSptCommand", GiveSptCommand); } private static registerLoaders(depContainer: DependencyContainer): void @@ -727,7 +753,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/ICommandoCommand.ts b/project/src/helpers/Dialogue/Commando/ICommandoCommand.ts new file mode 100644 index 00000000..03083f3b --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/ICommandoCommand.ts @@ -0,0 +1,10 @@ +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; + 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 new file mode 100644 index 00000000..82017853 --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/SptCommandoCommands.ts @@ -0,0 +1,67 @@ +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 { 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( + @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(); + } + + public getCommandPrefix(): string + { + return "spt"; + } + + public getCommands(): Set + { + return new Set(this.sptCommands.map((c) => c.getCommand())); + } + + public handle( + command: string, + commandHandler: IUserDialogInfo, + sessionId: string, + request: ISendMessageRequest, + ): string + { + 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 new file mode 100644 index 00000000..6c4cf12c --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts @@ -0,0 +1,138 @@ +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 performAction(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 doesn't exist in 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; + } + + 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)) + { + 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, + SpawnedInSession: true + }, + }; + 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..8aedd1ab --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/SptCommands/ISptCommand.ts @@ -0,0 +1,9 @@ +import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; +import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; + +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 new file mode 100644 index 00000000..e73cda1c --- /dev/null +++ b/project/src/helpers/Dialogue/CommandoDialogueChatBot.ts @@ -0,0 +1,74 @@ +import { inject, injectAll, injectable } from "tsyringe"; + +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 +{ + public constructor( + @inject("WinstonLogger") protected logger: ILogger, + @inject("MailSendService") protected mailSendService: MailSendService, + @injectAll("CommandoCommand") protected commandoCommands: ICommandoCommand[], + ) + { + } + + 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" }, + }; + } + + 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(" "); + + 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.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/models/spt/config/ICoreConfig.ts b/project/src/models/spt/config/ICoreConfig.ts index 3d9c6319..e5ef1e81 100644 --- a/project/src/models/spt/config/ICoreConfig.ts +++ b/project/src/models/spt/config/ICoreConfig.ts @@ -31,4 +31,18 @@ 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; } 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.`); } /** 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; } } 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; + } } 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