diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index 2fdfde9d..ba2c1223 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -90,7 +90,9 @@ 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 { GiveSptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand"; +import { ProfileSptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ProfileCommand/ProfileSptCommand"; +import { TraderSptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/TraderCommand/TraderSptCommand"; import { CommandoDialogueChatBot } from "@spt-aki/helpers/Dialogue/CommandoDialogueChatBot"; import { SptDialogueChatBot } from "@spt-aki/helpers/Dialogue/SptDialogueChatBot"; import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper"; @@ -375,6 +377,8 @@ export class Container // SptCommando Commands depContainer.registerType("SptCommand", "GiveSptCommand"); + depContainer.registerType("SptCommand", "TraderSptCommand"); + depContainer.registerType("SptCommand", "ProfileSptCommand"); } private static registerUtils(depContainer: DependencyContainer): void @@ -598,6 +602,12 @@ export class Container }); // SptCommands depContainer.register("GiveSptCommand", GiveSptCommand, { lifecycle: Lifecycle.Singleton }); + depContainer.register("TraderSptCommand", TraderSptCommand, { + lifecycle: Lifecycle.Singleton, + }); + depContainer.register("ProfileSptCommand", ProfileSptCommand, { + lifecycle: Lifecycle.Singleton, + }); } private static registerLoaders(depContainer: DependencyContainer): void diff --git a/project/src/helpers/Dialogue/AbstractDialogueChatBot.ts b/project/src/helpers/Dialogue/AbstractDialogueChatBot.ts index ed387b97..c5f9b5c1 100644 --- a/project/src/helpers/Dialogue/AbstractDialogueChatBot.ts +++ b/project/src/helpers/Dialogue/AbstractDialogueChatBot.ts @@ -57,12 +57,34 @@ export abstract class AbstractDialogueChatBot implements IDialogueChatBot if (splitCommand[0].toLowerCase() === "help") { - const helpMessage = this.chatCommands.map((c) => - `Available commands:\n\n${c.getCommandPrefix()}:\n\n${ - Array.from(c.getCommands()).map((command) => c.getCommandHelp(command)).join("\n") - }` - ).join("\n"); - this.mailSendService.sendUserMessageToPlayer(sessionId, this.getChatBot(), helpMessage); + this.mailSendService.sendUserMessageToPlayer( + sessionId, + this.getChatBot(), + "The available commands will be listed below:", + ); + // due to BSG being dumb with messages we need a mandatory timeout between messages so they get out on the right order + setTimeout(() => + { + for (const chatCommand of this.chatCommands) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + this.getChatBot(), + `Commands available for "${chatCommand.getCommandPrefix()}" prefix:`, + ); + setTimeout(() => + { + for (const subCommand of chatCommand.getCommands()) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + this.getChatBot(), + `Subcommand ${subCommand}:\n${chatCommand.getCommandHelp(subCommand)}`, + ); + } + }, 1000); + } + }, 1000); return request.dialogId; } diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts similarity index 96% rename from project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts rename to project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts index ccab75e2..9bfb7004 100644 --- a/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts +++ b/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts @@ -1,5 +1,5 @@ +import { SavedCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveCommand/SavedCommand"; import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand"; -import { SavedCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/SavedCommand"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { PresetHelper } from "@spt-aki/helpers/PresetHelper"; import { Item } from "@spt-aki/models/eft/common/tables/IItem"; @@ -107,6 +107,15 @@ export class GiveSptCommand implements ISptCommand isItemName = result[5] !== undefined; item = result[5] ? result[5] : result[2]; quantity = +result[6]; + if (quantity <= 0) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + `Invalid quantity! Must be 1 or higher. Use \"help\" for more information.`, + ); + return request.dialogId; + } if (isItemName) { diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/SavedCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/SavedCommand.ts similarity index 100% rename from project/src/helpers/Dialogue/Commando/SptCommands/SavedCommand.ts rename to project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/SavedCommand.ts diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/ProfileCommand/ProfileSptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/ProfileCommand/ProfileSptCommand.ts new file mode 100644 index 00000000..025a205c --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/SptCommands/ProfileCommand/ProfileSptCommand.ts @@ -0,0 +1,163 @@ +import { SavedCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveCommand/SavedCommand"; +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 { 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 { SkillTypes } from "@spt-aki/models/enums/SkillTypes"; +import { IProfileChangeEvent, ProfileChangeEventType } from "@spt-aki/models/spt/dialog/ISendMessageDetails"; +import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; +import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; +import { LocaleService } from "@spt-aki/services/LocaleService"; +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 ProfileSptCommand implements ISptCommand +{ + /** + * Regex to account for all these cases: + * spt profile level 20 + * spt profile skill metabolism 10 + */ + private static commandRegex = + /^spt profile (?level|skill)((?<=.*skill) (?[\w]+)){0,1} (?(?!0+)[0-9]+)$/; + + protected savedCommand: SavedCommand; + + 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, + @inject("LocaleService") protected localeService: LocaleService, + @inject("DatabaseServer") protected databaseServer: DatabaseServer, + @inject("ProfileHelper") protected profileHelper: ProfileHelper, + ) + { + } + + public getCommand(): string + { + return "profile"; + } + + public getCommandHelp(): string + { + return "spt profile\n========\nSets the profile level or skill to the desired level through the message system.\n\n\tspt profile level [desired level]\n\t\tEx: spt profile level 20\n\n\tspt profile skill [skill name] [quantity]\n\t\tEx: spt profile skill metabolism 51"; + } + + public performAction(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string + { + if (!ProfileSptCommand.commandRegex.test(request.text)) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + "Invalid use of trader command. Use \"help\" for more information.", + ); + return request.dialogId; + } + + const result = ProfileSptCommand.commandRegex.exec(request.text); + + const command: string = result.groups.command; + const skill: string = result.groups.skill; + const quantity: number = +result.groups.quantity; + + let event: IProfileChangeEvent; + switch (command) + { + case "level": + if (quantity < 1 || quantity > this.profileHelper.getMaxLevel()) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + "Invalid use of profile command, the level was outside bounds: 1 to 70. Use \"help\" for more information.", + ); + return request.dialogId; + } + event = this.handleLevelCommand(quantity); + break; + case "skill": + { + const enumSkill = Object.values(SkillTypes).find((t) => + t.toLocaleLowerCase() === skill.toLocaleLowerCase() + ); + + if (enumSkill === undefined) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + "Invalid use of profile command, the skill was not found. Use \"help\" for more information.", + ); + return request.dialogId; + } + + if (quantity < 0 || quantity > 51) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + "Invalid use of profile command, the skill level was outside bounds: 1 to 51. Use \"help\" for more information.", + ); + return request.dialogId; + } + event = this.handleSkillCommand(enumSkill, quantity); + break; + } + default: + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + `If you are reading this, this is bad. Please report this to SPT staff with a screenshot. Command ${command}.`, + ); + return request.dialogId; + } + + this.mailSendService.sendSystemMessageToPlayer( + sessionId, + "A single ruble is being attached, required by BSG logic.", + [{ + _id: this.hashUtil.generate(), + _tpl: "5449016a4bdc2d6f028b456f", + upd: { StackObjectsCount: 1 }, + parentId: this.hashUtil.generate(), + slotId: "main", + }], + undefined, + [event], + ); + return request.dialogId; + } + + protected handleSkillCommand(skill: string, level: number): IProfileChangeEvent + { + const event: IProfileChangeEvent = { + _id: this.hashUtil.generate(), + Type: ProfileChangeEventType.SKILL_POINTS, + value: level * 100, + entity: skill, + }; + return event; + } + + protected handleLevelCommand(level: number): IProfileChangeEvent + { + const exp = this.profileHelper.getExperience(level); + const event: IProfileChangeEvent = { + _id: this.hashUtil.generate(), + Type: ProfileChangeEventType.PROFILE_LEVEL, + value: exp, + entity: null, + }; + return event; + } +} diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/TraderCommand/TraderSptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/TraderCommand/TraderSptCommand.ts new file mode 100644 index 00000000..feb3c0b2 --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/SptCommands/TraderCommand/TraderSptCommand.ts @@ -0,0 +1,114 @@ +import { SavedCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveCommand/SavedCommand"; +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 { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; +import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; +import { IProfileChangeEvent, ProfileChangeEventType } from "@spt-aki/models/spt/dialog/ISendMessageDetails"; +import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; +import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; +import { LocaleService } from "@spt-aki/services/LocaleService"; +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 TraderSptCommand implements ISptCommand +{ + /** + * Regex to account for all these cases: + * spt trader prapor rep 100 + * spt trader mechanic spend 1000000 + */ + private static commandRegex = /^spt trader (?[\w]+) (?rep|spend) (?(?!0+)[0-9]+)$/; + + protected savedCommand: SavedCommand; + + 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, + @inject("LocaleService") protected localeService: LocaleService, + @inject("DatabaseServer") protected databaseServer: DatabaseServer, + ) + { + } + + public getCommand(): string + { + return "trader"; + } + + public getCommandHelp(): string + { + return "spt trader\n========\nSets the reputation or money spent to the input quantity through the message system.\n\n\tspt trader [trader] rep [quantity]\n\t\tEx: spt trader prapor rep 2\n\n\tspt trader [trader] spend [quantity]\n\t\tEx: spt trader therapist spend 1000000"; + } + + public performAction(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string + { + if (!TraderSptCommand.commandRegex.test(request.text)) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + "Invalid use of trader command. Use \"help\" for more information.", + ); + return request.dialogId; + } + + const result = TraderSptCommand.commandRegex.exec(request.text); + + const trader: string = result.groups.trader; + const command: string = result.groups.command; + const quantity: number = +result.groups.quantity; + + const dbTrader = Object.values(this.databaseServer.getTables().traders).find((t) => + t.base.nickname.toLocaleLowerCase() === trader.toLocaleLowerCase() + ); + if (dbTrader === undefined) + { + this.mailSendService.sendUserMessageToPlayer( + sessionId, + commandHandler, + "Invalid use of trader command, the trader was not found. Use \"help\" for more information.", + ); + return request.dialogId; + } + let profileChangeEventType: ProfileChangeEventType; + switch (command) + { + case "rep": + profileChangeEventType = ProfileChangeEventType.TRADER_STANDING; + break; + case "spend": + profileChangeEventType = ProfileChangeEventType.TRADER_SALES_SUM; + break; + } + + const event: IProfileChangeEvent = { + _id: this.hashUtil.generate(), + Type: profileChangeEventType, + value: quantity, + entity: dbTrader.base._id, + }; + + this.mailSendService.sendSystemMessageToPlayer( + sessionId, + "A single ruble is being attached, required by BSG logic.", + [{ + _id: this.hashUtil.generate(), + _tpl: "5449016a4bdc2d6f028b456f", + upd: { StackObjectsCount: 1 }, + parentId: this.hashUtil.generate(), + slotId: "main", + }], + undefined, + [event], + ); + return request.dialogId; + } +} diff --git a/project/src/models/spt/dialog/ISendMessageDetails.ts b/project/src/models/spt/dialog/ISendMessageDetails.ts index 1f9f0d05..13d8957e 100644 --- a/project/src/models/spt/dialog/ISendMessageDetails.ts +++ b/project/src/models/spt/dialog/ISendMessageDetails.ts @@ -34,7 +34,17 @@ export interface ISendMessageDetails export interface IProfileChangeEvent { _id: string; - Type: "TraderSalesSum" | "TraderStanding" | "ProfileLevel" | "SkillPoints" | "ExamineAllItems" | "UnlockTrader"; + Type: ProfileChangeEventType; value: number; entity?: string; } + +export enum ProfileChangeEventType +{ + TRADER_SALES_SUM = "TraderSalesSum", + TRADER_STANDING = "TraderStanding", + PROFILE_LEVEL = "ProfileLevel", + SKILL_POINTS = "SkillPoints", + EXAMINE_ALL_ITEMS = "ExamineAllItems", + UNLOCK_TRADER = "UnlockTrader", +} diff --git a/project/src/services/MailSendService.ts b/project/src/services/MailSendService.ts index 2660d9c7..4fbc536a 100644 --- a/project/src/services/MailSendService.ts +++ b/project/src/services/MailSendService.ts @@ -10,7 +10,7 @@ import { Dialogue, IUserDialogInfo, Message, MessageItems } from "@spt-aki/model import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { MessageType } from "@spt-aki/models/enums/MessageType"; import { Traders } from "@spt-aki/models/enums/Traders"; -import { ISendMessageDetails } from "@spt-aki/models/spt/dialog/ISendMessageDetails"; +import { IProfileChangeEvent, ISendMessageDetails } from "@spt-aki/models/spt/dialog/ISendMessageDetails"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; import { SaveServer } from "@spt-aki/servers/SaveServer"; @@ -169,7 +169,8 @@ export class MailSendService sessionId: string, message: string, items: Item[] = [], - maxStorageTimeSeconds = null, + maxStorageTimeSeconds?: number, + profileChangeEvents?: IProfileChangeEvent[], ): void { const details: ISendMessageDetails = { @@ -185,6 +186,11 @@ export class MailSendService details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; // 48 hours if no value supplied } + if ((profileChangeEvents?.length ?? 0) > 0) + { + details.profileChangeEvents = profileChangeEvents; + } + this.sendMessageToPlayer(details); } @@ -199,8 +205,8 @@ export class MailSendService sessionId: string, messageLocaleId: string, items: Item[] = [], - profileChangeEvents = [], - maxStorageTimeSeconds = null, + profileChangeEvents?: IProfileChangeEvent[], + maxStorageTimeSeconds?: number, ): void { const details: ISendMessageDetails = { @@ -216,7 +222,7 @@ export class MailSendService details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; // 48 hours if no value supplied } - if (profileChangeEvents.length > 0) + if ((profileChangeEvents?.length ?? 0) > 0) { details.profileChangeEvents = profileChangeEvents; }