From c5246d1387161107643e109568048391464a0ccf Mon Sep 17 00:00:00 2001 From: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com> Date: Sat, 15 Feb 2025 23:31:43 -0800 Subject: [PATCH] Refactor bot generation to be done more like live, to allow the client to cache bot data --- project/assets/configs/bot.json | 25 +-- project/assets/configs/pmc.json | 56 ------ project/src/callbacks/InraidCallbacks.ts | 4 +- project/src/controllers/BotController.ts | 186 ++++++------------ project/src/controllers/InraidController.ts | 4 +- project/src/generators/BotGenerator.ts | 6 + project/src/helpers/BotHelper.ts | 11 +- project/src/models/spt/config/IBotConfig.ts | 7 - project/src/models/spt/config/IPmcConfig.ts | 2 - .../src/routers/static/InraidStaticRouter.ts | 4 +- 10 files changed, 78 insertions(+), 227 deletions(-) diff --git a/project/assets/configs/bot.json b/project/assets/configs/bot.json index 1f02fae45..8be26c577 100644 --- a/project/assets/configs/bot.json +++ b/project/assets/configs/bot.json @@ -71,7 +71,9 @@ "bossBoar", "bossBoarSniper", "bossKolontay", - "bossPartisan" + "bossPartisan", + "followerBigPipe", + "followerBirdEye" ], "durability": { "default": { @@ -2954,27 +2956,6 @@ "55d4af3a4bdc2d972f8b456f" ], "disableLootOnBotTypes": [], - "assaultToBossConversion": { - "bossConvertEnabled": false, - "bossesToConvertToWeights": { - "bossKilla": 1, - "bossSanitar": 1, - "bossTagilla": 1, - "bossBully": 1, - "bossGluhar": 1, - "bossKojaniy": 1, - "bossKolontay": 1, - "bossKnight": 1, - "followerBigPipe": 1, - "followerBirdEye": 1 - }, - "bossConvertMinMax": { - "assault": { - "min": 100, - "max": 100 - } - } - }, "botNameLengthLimit": 19, "botRolesThatMustHaveUniqueName": ["assault", "pmcusec", "pmcbear"] } diff --git a/project/assets/configs/pmc.json b/project/assets/configs/pmc.json index 3b610b828..6d8f13cd5 100644 --- a/project/assets/configs/pmc.json +++ b/project/assets/configs/pmc.json @@ -759,62 +759,6 @@ ], "maxPocketLootTotalRub": 50000, "maxVestLootTotalRub": 50000, - "convertIntoPmcChance": { - "default": { - "assault": { - "min": 15, - "max": 23 - }, - "cursedassault": { - "min": 0, - "max": 0 - }, - "pmcbot": { - "min": 5, - "max": 10 - }, - "exusec": { - "min": 5, - "max": 5 - }, - "arenafighter": { - "min": 0, - "max": 0 - }, - "arenafighterevent": { - "min": 0, - "max": 0 - }, - "crazyassaultevent": { - "min": 0, - "max": 0 - } - }, - "factory4_day": { - "assault": { - "min": 10, - "max": 15 - } - }, - "laboratory": { - "pmcbot": { - "min": 1, - "max": 5 - } - }, - "rezervbase": { - "pmcbot": { - "min": 0, - "max": 0 - } - }, - "lighthouse": { - "exusec": { - "min": 1, - "max": 1 - } - } - }, "hostilitySettings": { "pmcusec": { "additionalEnemyTypes": [ diff --git a/project/src/callbacks/InraidCallbacks.ts b/project/src/callbacks/InraidCallbacks.ts index bf2a4a55d..8fc66287d 100644 --- a/project/src/callbacks/InraidCallbacks.ts +++ b/project/src/callbacks/InraidCallbacks.ts @@ -53,7 +53,7 @@ export class InraidCallbacks { return this.httpResponse.noBody(this.inraidController.getTraitorScavHostileChance(url, sessionId)); } - public getBossConvertSettings(url: string, info: IEmptyRequestData, sessionId: string): string { - return this.httpResponse.noBody(this.inraidController.getBossConvertSettings(url, sessionId)); + public getBossTypes(url: string, info: IEmptyRequestData, sessionId: string): string { + return this.httpResponse.noBody(this.inraidController.getBossTypes(url, sessionId)); } } diff --git a/project/src/controllers/BotController.ts b/project/src/controllers/BotController.ts index ec9936608..9f2ef722b 100644 --- a/project/src/controllers/BotController.ts +++ b/project/src/controllers/BotController.ts @@ -155,27 +155,44 @@ export class BotController { public async generate(sessionId: string, info: IGenerateBotsRequestData): Promise { const pmcProfile = this.profileHelper.getPmcProfile(sessionId); - // Use this opportunity to create and cache bots for later retreval - const multipleBotTypesRequested = info.conditions.length > 1; - if (multipleBotTypesRequested) { - return this.generateMultipleBotsAndCache(info, pmcProfile, sessionId); + // If we don't have enough bots cached to satisfy this request, populate the cache + if (!this.cacheSatisfiesRequest(info)) + { + await this.generateAndCacheBots(info, pmcProfile, sessionId); } - return this.returnSingleBotFromCache(sessionId, info); + return this.returnBotsFromCache(info); } /** - * On first bot generation bots are generated and stored inside a cache, ready to be used later + * Return true if the current cache satisfies the passed in bot generation request + * @param info + * @returns + */ + public cacheSatisfiesRequest(info: IGenerateBotsRequestData): boolean { + return info.conditions.every((condition) => { + // Create the key that would be used for caching this bot type, so we can check how many exist + const cacheKey = this.botGenerationCacheService.createCacheKey( + condition.Role, + condition.Difficulty + ); + + return this.botGenerationCacheService.getCachedBotCount(cacheKey) >= condition.Limit; + }); + } + + /** + * When we have less bots than necessary to fulfill a request, re-populate the cache * @param request Bot generation request object * @param pmcProfile Player profile * @param sessionId Session id * @returns IBotBase[] */ - protected async generateMultipleBotsAndCache( + protected async generateAndCacheBots( request: IGenerateBotsRequestData, - pmcProfile: IPmcData, + pmcProfile: IPmcData | undefined, sessionId: string, - ): Promise { + ): Promise { const raidSettings = this.getMostRecentRaidSettings(); const allPmcsHaveSameNameAsPlayer = this.randomUtil.getChance100( @@ -184,12 +201,24 @@ export class BotController { // Map conditions to promises for bot generation const conditionPromises = request.conditions.map(async (condition) => { + // If we already have enough for this bot type, don't generate more + const cacheKey = this.botGenerationCacheService.createCacheKey( + condition.Role, + condition.Difficulty + ); + + if (this.botGenerationCacheService.getCachedBotCount(cacheKey) >= condition.Limit) + { + return; + } + const botGenerationDetails = this.getBotGenerationDetailsForWave( condition, pmcProfile, allPmcsHaveSameNameAsPlayer, raidSettings, - this.getBotPresetGenerationLimit(condition.Role), + // Spawn the higher of the preset cache amount, or the requested amount + Math.max(this.getBotPresetGenerationLimit(condition.Role), condition.Limit), this.botHelper.isBotPmc(condition.Role), ); @@ -198,7 +227,6 @@ export class BotController { }); await Promise.all(conditionPromises); - return []; } protected getMostRecentRaidSettings(): IGetRaidConfigurationRequestData { @@ -238,7 +266,7 @@ export class BotController { */ protected getBotGenerationDetailsForWave( condition: ICondition, - pmcProfile: IPmcData, + pmcProfile: IPmcData | undefined, allPmcsHaveSameNameAsPlayer: boolean, raidSettings: IGetRaidConfigurationRequestData, botCountToGenerate: number, @@ -249,7 +277,7 @@ export class BotController { side: generateAsPmc ? this.botHelper.getPmcSideByRole(condition.Role) : SideType.SAVAGE, role: condition.Role, playerLevel: this.getPlayerLevelFromProfile(pmcProfile), - playerName: pmcProfile.Info.Nickname, + playerName: pmcProfile?.Info.Nickname, botRelativeLevelDeltaMax: this.pmcConfig.botRelativeLevelDeltaMax, botRelativeLevelDeltaMin: this.pmcConfig.botRelativeLevelDeltaMin, botCountToGenerate: botCountToGenerate, @@ -265,8 +293,8 @@ export class BotController { * @param pmcProfile Profile to get level from * @returns Level as number */ - protected getPlayerLevelFromProfile(pmcProfile: IPmcData): number { - return pmcProfile.Info.Level; + protected getPlayerLevelFromProfile(pmcProfile: IPmcData | undefined): number { + return pmcProfile?.Info.Level || 1; } /** @@ -350,124 +378,34 @@ export class BotController { } /** - * Pull a single bot out of cache and return, if cache is empty add bots to it and then return + * Return the bots requested by the given bot generation request * @param sessionId Session id * @param request Bot generation request object - * @returns Single IBotBase object + * @returns An array of IBotBase objects as requested by request */ - protected async returnSingleBotFromCache( - sessionId: string, + protected async returnBotsFromCache( request: IGenerateBotsRequestData, ): Promise { - const pmcProfile = this.profileHelper.getPmcProfile(sessionId); - const requestedBot = request.conditions[0]; - - const raidSettings = this.getMostRecentRaidSettings(); - - // Create generation request for when cache is empty - const condition: ICondition = { - Role: requestedBot.Role, - Limit: 5, - Difficulty: requestedBot.Difficulty, - }; - const botGenerationDetails = this.getBotGenerationDetailsForWave( - condition, - pmcProfile, - false, - raidSettings, - this.getBotPresetGenerationLimit(requestedBot.Role), - this.botHelper.isBotPmc(requestedBot.Role), - ); - - // Event bots need special actions to occur, set data up for them - const isEventBot = requestedBot.Role.toLowerCase().includes("event"); - if (isEventBot) { - // Add eventRole data + reassign role property - botGenerationDetails.eventRole = requestedBot.Role; - botGenerationDetails.role = this.seasonalEventService.getBaseRoleForEventBot( - botGenerationDetails.eventRole, + const desiredBots: IBotBase[] = []; + + // We can assume that during this call, we have enough bots cached to cover the request + request.conditions.map((requestedBot) => { + // Create a compound key to store bots in cache against + const cacheKey = this.botGenerationCacheService.createCacheKey( + requestedBot.Role, + requestedBot.Difficulty, ); - } - // Does non pmc bot have a chance of being converted into a pmc - const convertIntoPmcChanceMinMax = this.getPmcConversionMinMaxForLocation( - requestedBot.Role, - raidSettings?.location, - ); - if (convertIntoPmcChanceMinMax && !botGenerationDetails.isPmc) { - // Bot has % chance to become pmc and isnt one pmc already - const convertToPmc = this.botHelper.rollChanceToBePmc(convertIntoPmcChanceMinMax); - if (convertToPmc) { - // Update requirements - botGenerationDetails.isPmc = true; - botGenerationDetails.role = this.botHelper.getRandomizedPmcRole(); - botGenerationDetails.side = this.botHelper.getPmcSideByRole(botGenerationDetails.role); - botGenerationDetails.botDifficulty = this.getPMCDifficulty(requestedBot.Difficulty); - botGenerationDetails.botCountToGenerate = this.getBotPresetGenerationLimit(botGenerationDetails.role); - } - } - // Only convert to boss when not already converted to PMC & Boss Convert is enabled - const { bossConvertEnabled, bossConvertMinMax, bossesToConvertToWeights } = - this.botConfig.assaultToBossConversion; - if (bossConvertEnabled && !botGenerationDetails.isPmc) { - const bossConvertPercent = bossConvertMinMax[requestedBot.Role.toLowerCase()]; - if (bossConvertPercent) { - // Roll a percentage check if we should convert scav to boss - if ( - this.randomUtil.getChance100(this.randomUtil.getInt(bossConvertPercent.min, bossConvertPercent.max)) - ) { - this.updateBotGenerationDetailsToRandomBoss(botGenerationDetails, bossesToConvertToWeights); - } + // Fetch enough bots to satisfy the request + for (let i = 0; i < requestedBot.Limit; i++) + { + const desiredBot = this.botGenerationCacheService.getBot(cacheKey); + this.botGenerationCacheService.storeUsedBot(desiredBot); + desiredBots.push(desiredBot); } - } - - // Create a compound key to store bots in cache against - const cacheKey = this.botGenerationCacheService.createCacheKey( - botGenerationDetails.eventRole ?? botGenerationDetails.role, - botGenerationDetails.botDifficulty, - ); - - // Check cache for bot using above key - if (!this.botGenerationCacheService.cacheHasBotWithKey(cacheKey)) { - // No bot in cache, generate new and store in cache - await Promise.all( - Array.from({ length: botGenerationDetails.botCountToGenerate }).map( - async () => await this.generateSingleBotAndStoreInCache(botGenerationDetails, sessionId, cacheKey), - ), - ); - - this.logger.debug( - `Generated ${botGenerationDetails.botCountToGenerate} ${botGenerationDetails.role} (${ - botGenerationDetails.eventRole ?? "" - }) ${botGenerationDetails.botDifficulty} bots`, - ); - } - - const desiredBot = this.botGenerationCacheService.getBot(cacheKey); - this.botGenerationCacheService.storeUsedBot(desiredBot); - - return [desiredBot]; - } - - protected getPmcConversionMinMaxForLocation(requestedBotRole: string, location: string): MinMax { - const mapSpecificConversionValues = this.pmcConfig.convertIntoPmcChance[location?.toLowerCase()]; - if (!mapSpecificConversionValues) { - return this.pmcConfig.convertIntoPmcChance.default[requestedBotRole]; - } - - return mapSpecificConversionValues[requestedBotRole?.toLowerCase()]; - } + }); - protected updateBotGenerationDetailsToRandomBoss( - botGenerationDetails: IBotGenerationDetails, - possibleBossTypeWeights: Record, - ): void { - // Seems Actual bosses have the same Brain issues like PMC gaining Boss Brains We cant use all bosses - botGenerationDetails.role = this.weightedRandomHelper.getWeightedValue(possibleBossTypeWeights); - - // Bosses are only ever 'normal' - botGenerationDetails.botDifficulty = "normal"; - botGenerationDetails.botCountToGenerate = this.getBotPresetGenerationLimit(botGenerationDetails.role); + return desiredBots; } /** diff --git a/project/src/controllers/InraidController.ts b/project/src/controllers/InraidController.ts index 0a7ef8460..f220f4efa 100644 --- a/project/src/controllers/InraidController.ts +++ b/project/src/controllers/InraidController.ts @@ -83,7 +83,7 @@ export class InraidController { return this.inRaidConfig.playerScavHostileChancePercent; } - public getBossConvertSettings(url: string, sessionId: string): string[] { - return Object.keys(this.botConfig.assaultToBossConversion.bossesToConvertToWeights); + public getBossTypes(url: string, sessionId: string): string[] { + return this.botConfig.bosses; } } diff --git a/project/src/generators/BotGenerator.ts b/project/src/generators/BotGenerator.ts index 1649d9c08..381ec5283 100644 --- a/project/src/generators/BotGenerator.ts +++ b/project/src/generators/BotGenerator.ts @@ -130,6 +130,12 @@ export class BotGenerator { this.logger.error(`Unable to retrieve: ${botRole} bot template, cannot generate bot of this type`); } + // The client expects the Side for PMCs to be Savage + if (botRole === "Bear" || botRole === "Usec") + { + preparedBotBase.Info.Side = "Savage"; + } + return this.generateBot(sessionId, preparedBotBase, botJsonTemplateClone, botGenerationDetails); } diff --git a/project/src/helpers/BotHelper.ts b/project/src/helpers/BotHelper.ts index db7d0ded0..84e0c2492 100644 --- a/project/src/helpers/BotHelper.ts +++ b/project/src/helpers/BotHelper.ts @@ -43,7 +43,7 @@ export class BotHelper { } public isBotBoss(botRole: string): boolean { - return this.botConfig.bosses.some((x) => x.toLowerCase() === botRole?.toLowerCase()); + return !this.isBotFollower(botRole) && this.botConfig.bosses.some((x) => x.toLowerCase() === botRole?.toLowerCase()); } public isBotFollower(botRole: string): boolean { @@ -96,15 +96,6 @@ export class BotHelper { return this.randomUtil.getChance100(this.randomUtil.getInt(botConvertMinMax.min, botConvertMinMax.max)); } - protected getPmcConversionValuesForLocation(location: string) { - const result = this.pmcConfig.convertIntoPmcChance[location.toLowerCase()]; - if (!result) { - this.pmcConfig.convertIntoPmcChance.default; - } - - return result; - } - /** * is the provided role a PMC, case-agnostic * @param botRole Role to check diff --git a/project/src/models/spt/config/IBotConfig.ts b/project/src/models/spt/config/IBotConfig.ts index a1840b192..fc6295be9 100644 --- a/project/src/models/spt/config/IBotConfig.ts +++ b/project/src/models/spt/config/IBotConfig.ts @@ -44,19 +44,12 @@ export interface IBotConfig extends IBaseConfig { lowProfileGasBlockTpls: string[]; /** What bottypes should be excluded from having loot generated on them (backpack/pocket/vest) does not disable food/drink/special/ */ disableLootOnBotTypes: string[]; - assaultToBossConversion: IAssaultToBossConversion; /** Max length a bots name can be */ botNameLengthLimit: number; /** Bot roles that must have a unique name when generated vs other bots in raid */ botRolesThatMustHaveUniqueName: string[]; } -export interface IAssaultToBossConversion { - bossConvertEnabled: boolean; - bossesToConvertToWeights: Record; - bossConvertMinMax: Record; -} - /** Number of bots to generate and store in cache on raid start per bot type */ export interface IPresetBatch { assault: number; diff --git a/project/src/models/spt/config/IPmcConfig.ts b/project/src/models/spt/config/IPmcConfig.ts index 2d7a1b5f3..303f2a987 100644 --- a/project/src/models/spt/config/IPmcConfig.ts +++ b/project/src/models/spt/config/IPmcConfig.ts @@ -38,8 +38,6 @@ export interface IPmcConfig extends IBaseConfig { lootItemLimitsRub: IMinMaxLootItemValue[]; maxPocketLootTotalRub: number; maxVestLootTotalRub: number; - /** Percentage chance a bot from a wave is converted into a PMC, first key = map, second key = bot wildspawn type (assault/exusec), value: min+max chance to be converted */ - convertIntoPmcChance: Record>; /** How many levels above player level can a PMC be */ botRelativeLevelDeltaMax: number; /** How many levels below player level can a PMC be */ diff --git a/project/src/routers/static/InraidStaticRouter.ts b/project/src/routers/static/InraidStaticRouter.ts index 1acc0a4c0..9c89983a0 100644 --- a/project/src/routers/static/InraidStaticRouter.ts +++ b/project/src/routers/static/InraidStaticRouter.ts @@ -26,9 +26,9 @@ export class InraidStaticRouter extends StaticRouter { }, ), new RouteAction( - "/singleplayer/bossconvert", + "/singleplayer/bosstypes", async (url: string, info: any, sessionID: string, output: string): Promise => { - return this.inraidCallbacks.getBossConvertSettings(url, info, sessionID); + return this.inraidCallbacks.getBossTypes(url, info, sessionID); }, ), ]);