diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index eb7e262c..5c4adc04 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -201,6 +201,7 @@ import { BotEquipmentFilterService } from "@spt/services/BotEquipmentFilterServi import { BotEquipmentModPoolService } from "@spt/services/BotEquipmentModPoolService"; import { BotGenerationCacheService } from "@spt/services/BotGenerationCacheService"; import { BotLootCacheService } from "@spt/services/BotLootCacheService"; +import { BotNameService } from "@spt/services/BotNameService"; import { BotWeaponModLimitService } from "@spt/services/BotWeaponModLimitService"; import { CircleOfCultistService } from "@spt/services/CircleOfCultistService"; import { CustomLocationWaveService } from "@spt/services/CustomLocationWaveService"; @@ -797,6 +798,9 @@ export class Container { depContainer.register("CircleOfCultistService", CircleOfCultistService, { lifecycle: Lifecycle.Singleton, }); + depContainer.register("BotNameService", BotNameService, { + lifecycle: Lifecycle.Singleton, + }); } private static registerServers(depContainer: DependencyContainer): void { diff --git a/project/src/generators/BotGenerator.ts b/project/src/generators/BotGenerator.ts index f392778a..7fc0a433 100644 --- a/project/src/generators/BotGenerator.ts +++ b/project/src/generators/BotGenerator.ts @@ -1,6 +1,5 @@ import { BotInventoryGenerator } from "@spt/generators/BotInventoryGenerator"; import { BotLevelGenerator } from "@spt/generators/BotLevelGenerator"; -import { BotDifficultyHelper } from "@spt/helpers/BotDifficultyHelper"; import { BotHelper } from "@spt/helpers/BotHelper"; import { ProfileHelper } from "@spt/helpers/ProfileHelper"; import { WeightedRandomHelper } from "@spt/helpers/WeightedRandomHelper"; @@ -27,9 +26,9 @@ import { IPmcConfig } from "@spt/models/spt/config/IPmcConfig"; import { ILogger } from "@spt/models/spt/utils/ILogger"; import { ConfigServer } from "@spt/servers/ConfigServer"; import { BotEquipmentFilterService } from "@spt/services/BotEquipmentFilterService"; +import { BotNameService } from "@spt/services/BotNameService"; import { DatabaseService } from "@spt/services/DatabaseService"; import { ItemFilterService } from "@spt/services/ItemFilterService"; -import { LocalisationService } from "@spt/services/LocalisationService"; import { SeasonalEventService } from "@spt/services/SeasonalEventService"; import { HashUtil } from "@spt/utils/HashUtil"; import { RandomUtil } from "@spt/utils/RandomUtil"; @@ -54,10 +53,9 @@ export class BotGenerator { @inject("BotEquipmentFilterService") protected botEquipmentFilterService: BotEquipmentFilterService, @inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper, @inject("BotHelper") protected botHelper: BotHelper, - @inject("BotDifficultyHelper") protected botDifficultyHelper: BotDifficultyHelper, @inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService, - @inject("LocalisationService") protected localisationService: LocalisationService, @inject("ItemFilterService") protected itemFilterService: ItemFilterService, + @inject("BotNameService") protected botNameService: BotNameService, @inject("ConfigServer") protected configServer: ConfigServer, @inject("PrimaryCloner") protected cloner: ICloner, ) { @@ -170,7 +168,14 @@ export class BotGenerator { ); } - bot.Info.Nickname = this.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole, sessionId); + bot.Info.Nickname = this.botNameService.generateUniqueBotNickname( + botJsonTemplate.firstName, + botJsonTemplate.lastName, + botGenerationDetails, + botRole, + ["assault", "pmcusec", "pmcbear"], + sessionId, + ); if (!this.seasonalEventService.christmasEventEnabled()) { // Process all bots EXCEPT gifter, he needs christmas items @@ -299,62 +304,6 @@ export class BotGenerator { } } - /** - * Create a bot nickname - * @param botJsonTemplate x.json from database - * @param botGenerationDetails - * @param botRole role of bot e.g. assault - * @param sessionId OPTIONAL: profile session id - * @returns Nickname for bot - */ - protected generateBotNickname( - botJsonTemplate: IBotType, - botGenerationDetails: BotGenerationDetails, - botRole: string, - sessionId?: string, - ): string { - const isPlayerScav = botGenerationDetails.isPlayerScav; - - let name = `${this.randomUtil.getArrayValue(botJsonTemplate.firstName)} ${ - this.randomUtil.getArrayValue(botJsonTemplate.lastName) || "" - }`; - name = name.trim(); - - // Simulate bot looking like a player scav with the PMC name in brackets. - // E.g. "ScavName (PMCName)" - if (this.shouldSimulatePlayerScavName(botRole, isPlayerScav)) { - return this.addPlayerScavNameSimulationSuffix(name); - } - - if (this.botConfig.showTypeInNickname && !isPlayerScav) { - name += ` ${botRole}`; - } - - // We want to replace pmc bot names with player name + prefix - if (botGenerationDetails.isPmc && botGenerationDetails.allPmcsHaveSameNameAsPlayer) { - const prefix = this.localisationService.getRandomTextThatMatchesPartialKey("pmc-name_prefix_"); - name = `${prefix} ${name}`; - } - - return name; - } - - protected shouldSimulatePlayerScavName(botRole: string, isPlayerScav: boolean): boolean { - return ( - botRole === "assault" && - this.randomUtil.getChance100(this.botConfig.chanceAssaultScavHasPlayerScavName) && - !isPlayerScav - ); - } - - protected addPlayerScavNameSimulationSuffix(nickname: string): string { - const pmcNames = [ - ...this.databaseService.getBots().types.usec.firstName, - ...this.databaseService.getBots().types.bear.firstName, - ]; - return `${nickname} (${this.randomUtil.getArrayValue(pmcNames)})`; - } - /** * Log the number of PMCs generated to the debug console * @param output Generated bot array, ready to send to client diff --git a/project/src/services/BotNameService.ts b/project/src/services/BotNameService.ts new file mode 100644 index 00000000..2c289fc1 --- /dev/null +++ b/project/src/services/BotNameService.ts @@ -0,0 +1,129 @@ +import { ProfileHelper } from "@spt/helpers/ProfileHelper"; +import { ConfigTypes } from "@spt/models/enums/ConfigTypes"; +import { BotGenerationDetails } from "@spt/models/spt/bots/BotGenerationDetails"; +import { IBotConfig } from "@spt/models/spt/config/IBotConfig"; +import { IPmcConfig } from "@spt/models/spt/config/IPmcConfig"; +import { ILogger } from "@spt/models/spt/utils/ILogger"; +import { ConfigServer } from "@spt/servers/ConfigServer"; +import { RandomUtil } from "@spt/utils/RandomUtil"; +import { ICloner } from "@spt/utils/cloners/ICloner"; +import { inject, injectable } from "tsyringe"; +import { DatabaseService } from "./DatabaseService"; +import { LocalisationService } from "./LocalisationService"; + +@injectable() +export class BotNameService { + protected botConfig: IBotConfig; + protected pmcConfig: IPmcConfig; + protected usedNameCache: Set; + + constructor( + @inject("PrimaryLogger") protected logger: ILogger, + @inject("RandomUtil") protected randomUtil: RandomUtil, + @inject("ProfileHelper") protected profileHelper: ProfileHelper, + @inject("DatabaseService") protected databaseService: DatabaseService, + @inject("LocalisationService") protected localisationService: LocalisationService, + @inject("ConfigServer") protected configServer: ConfigServer, + @inject("PrimaryCloner") protected cloner: ICloner, + ) { + this.botConfig = this.configServer.getConfig(ConfigTypes.BOT); + this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC); + + this.usedNameCache = new Set(); + } + + /** + * Clear out any entries in Name Set + */ + public clearNameCache() { + this.usedNameCache.clear(); + } + + /** + * Create a unique bot nickname + * @param firstNames FIRST names to choose from + * @param lastNames OPTIONAL: Names to choose from + * @param botGenerationDetails + * @param botRole role of bot e.g. assault + * @param uniqueRoles Lowercase roles to always make unique + * @param sessionId OPTIONAL: profile session id + * @returns Nickname for bot + */ + public generateUniqueBotNickname( + firstNames: string[], + lastNames: string[], + botGenerationDetails: BotGenerationDetails, + botRole: string, + uniqueRoles?: string[], + sessionId?: string, + ): string { + let isUnique = true; + let attempts = 0; + + while (attempts < 5) { + const isPlayerScav = botGenerationDetails.isPlayerScav; + const simulateScavName = isPlayerScav ? false : this.shouldSimulatePlayerScavName(botRole); + + // Get basic name with no whitespace trimmed off sides + let name = `${this.randomUtil.getArrayValue(firstNames)} ${this.randomUtil.getArrayValue(lastNames) || ""}`; + name = name.trim(); + + // Simulate bot looking like a player scav with the PMC name in brackets. + // E.g. "ScavName (PMC Name)" + if (simulateScavName) { + return this.addPlayerScavNameSimulationSuffix(name); + } + + // Config is set to add role to end of bot name + if (this.botConfig.showTypeInNickname && !isPlayerScav) { + name += ` ${botRole}`; + } + + // Replace pmc bot names with player name + prefix + if (botGenerationDetails.isPmc && botGenerationDetails.allPmcsHaveSameNameAsPlayer) { + const prefix = this.localisationService.getRandomTextThatMatchesPartialKey("pmc-name_prefix_"); + name = `${prefix} ${name}`; + } + + // Is this a role that must be unique + if (uniqueRoles.includes(botRole.toLowerCase())) { + // Check name in cache + isUnique = !this.usedNameCache.has(name); + if (!isUnique) { + // Not unique + if (attempts >= 5) { + // 5 attempts to generate a name, pool probably isn't big enough + this.logger.debug(`Failed to find unique name for: ${name} after 5 attempts`); + } + + attempts++; + + // Try again + continue; + } + } + + // Add bot name to cache to prevent being used again + this.usedNameCache.add(name); + + return name; + } + } + + /** + * Should this bot have a name like "name (Pmc Name)" + * @param botRole Role bot has + * @returns True if name should be simulated pscav + */ + protected shouldSimulatePlayerScavName(botRole: string): boolean { + return botRole === "assault" && this.randomUtil.getChance100(this.botConfig.chanceAssaultScavHasPlayerScavName); + } + + protected addPlayerScavNameSimulationSuffix(nickname: string): string { + const pmcNames = [ + ...this.databaseService.getBots().types.usec.firstName, + ...this.databaseService.getBots().types.bear.firstName, + ]; + return `${nickname} (${this.randomUtil.getArrayValue(pmcNames)})`; + } +} diff --git a/project/src/services/LocationLifecycleService.ts b/project/src/services/LocationLifecycleService.ts index 3048f888..548f03fa 100644 --- a/project/src/services/LocationLifecycleService.ts +++ b/project/src/services/LocationLifecycleService.ts @@ -29,6 +29,7 @@ import { ConfigServer } from "@spt/servers/ConfigServer"; import { SaveServer } from "@spt/servers/SaveServer"; import { BotGenerationCacheService } from "@spt/services/BotGenerationCacheService"; import { BotLootCacheService } from "@spt/services/BotLootCacheService"; +import { BotNameService } from "@spt/services/BotNameService"; import { DatabaseService } from "@spt/services/DatabaseService"; import { InsuranceService } from "@spt/services/InsuranceService"; import { LocalisationService } from "@spt/services/LocalisationService"; @@ -71,6 +72,7 @@ export class LocationLifecycleService { @inject("BotGenerationCacheService") protected botGenerationCacheService: BotGenerationCacheService, @inject("MailSendService") protected mailSendService: MailSendService, @inject("RaidTimeAdjustmentService") protected raidTimeAdjustmentService: RaidTimeAdjustmentService, + @inject("BotNameService") protected botNameService: BotNameService, @inject("LootGenerator") protected lootGenerator: LootGenerator, @inject("ApplicationContext") protected applicationContext: ApplicationContext, @inject("LocationLootGenerator") protected locationLootGenerator: LocationLootGenerator, @@ -95,6 +97,7 @@ export class LocationLifecycleService { // Clear bot cache ready for a fresh raid this.botGenerationCacheService.clearStoredBots(); + this.botNameService.clearNameCache(); return result; }