Server/project/src/controllers/BotController.ts

527 lines
20 KiB
TypeScript
Raw Normal View History

2023-03-03 15:23:46 +00:00
import { inject, injectable } from "tsyringe";
import { ApplicationContext } from "@spt/context/ApplicationContext";
import { ContextVariableType } from "@spt/context/ContextVariableType";
import { BotGenerator } from "@spt/generators/BotGenerator";
import { BotDifficultyHelper } from "@spt/helpers/BotDifficultyHelper";
import { BotHelper } from "@spt/helpers/BotHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { MinMax } from "@spt/models/common/MinMax";
import { Condition, IGenerateBotsRequestData } from "@spt/models/eft/bot/IGenerateBotsRequestData";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IBotBase } from "@spt/models/eft/common/tables/IBotBase";
import { IBotCore } from "@spt/models/eft/common/tables/IBotCore";
import { Difficulty } from "@spt/models/eft/common/tables/IBotType";
import { IGetRaidConfigurationRequestData } from "@spt/models/eft/match/IGetRaidConfigurationRequestData";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { WildSpawnTypeNumber } from "@spt/models/enums/WildSpawnTypeNumber";
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 { DatabaseServer } from "@spt/servers/DatabaseServer";
import { BotGenerationCacheService } from "@spt/services/BotGenerationCacheService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { MatchBotDetailsCacheService } from "@spt/services/MatchBotDetailsCacheService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { RandomUtil } from "@spt/utils/RandomUtil";
2023-03-03 15:23:46 +00:00
@injectable()
export class BotController
{
protected botConfig: IBotConfig;
protected pmcConfig: IPmcConfig;
2023-03-03 15:23:46 +00:00
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("BotGenerator") protected botGenerator: BotGenerator,
@inject("BotHelper") protected botHelper: BotHelper,
@inject("BotDifficultyHelper") protected botDifficultyHelper: BotDifficultyHelper,
@inject("BotGenerationCacheService") protected botGenerationCacheService: BotGenerationCacheService,
@inject("MatchBotDetailsCacheService") protected matchBotDetailsCacheService: MatchBotDetailsCacheService,
2023-03-03 15:23:46 +00:00
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
2023-03-03 15:23:46 +00:00
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("ApplicationContext") protected applicationContext: ApplicationContext,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("RecursiveCloner") protected cloner: ICloner,
2023-03-03 15:23:46 +00:00
)
{
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
2023-03-03 15:23:46 +00:00
}
/**
2023-11-10 16:49:29 -05:00
* Return the number of bot load-out varieties to be generated
* @param type bot Type we want the load-out gen count for
2023-03-03 15:23:46 +00:00
* @returns number of bots to generate
*/
public getBotPresetGenerationLimit(type: string): number
{
const value = this.botConfig.presetBatch[type === "assaultGroup" ? "assault" : type];
if (!value)
{
2024-05-21 14:28:52 +01:00
this.logger.warning(this.localisationService.getText("bot-bot_preset_count_value_missing", type));
2024-01-07 19:29:42 +00:00
return 30;
}
2024-05-21 14:28:52 +01:00
return value;
2023-03-03 15:23:46 +00:00
}
/**
2023-07-15 14:49:25 +01:00
* Handle singleplayer/settings/bot/difficulty
2023-11-10 16:49:29 -05:00
* Get the core.json difficulty settings from database/bots
2023-03-03 15:23:46 +00:00
* @returns IBotCore
*/
public getBotCoreDifficulty(): IBotCore
{
return this.databaseServer.getTables().bots!.core;
2023-03-03 15:23:46 +00:00
}
/**
* Get bot difficulty settings
* Adjust PMC settings to ensure they engage the correct bot types
2023-03-03 15:23:46 +00:00
* @param type what bot the server is requesting settings for
* @param diffLevel difficulty level server requested settings for
* @param ignoreRaidSettings should raid settings chosen pre-raid be ignored
2023-03-03 15:23:46 +00:00
* @returns Difficulty object
*/
public getBotDifficulty(type: string, diffLevel: string, ignoreRaidSettings = false): Difficulty
2023-03-03 15:23:46 +00:00
{
let difficulty = diffLevel.toLowerCase();
const raidConfig = this.applicationContext
.getLatestValue(ContextVariableType.RAID_CONFIGURATION)
?.getValue<IGetRaidConfigurationRequestData>();
if (!(raidConfig || ignoreRaidSettings))
{
2023-11-10 16:49:29 -05:00
this.logger.error(
this.localisationService.getText("bot-missing_application_context", "RAID_CONFIGURATION"),
);
}
2023-03-03 15:23:46 +00:00
// Check value chosen in pre-raid difficulty dropdown
// If value is not 'asonline', change requested difficulty to be what was chosen in dropdown
const botDifficultyDropDownValue = raidConfig?.wavesSettings.botDifficulty.toLowerCase() ?? "asonline";
2023-03-03 15:23:46 +00:00
if (botDifficultyDropDownValue !== "asonline")
{
difficulty
= this.botDifficultyHelper.convertBotDifficultyDropdownToBotDifficulty(botDifficultyDropDownValue);
2023-03-03 15:23:46 +00:00
}
let difficultySettings: Difficulty;
const lowercasedBotType = type.toLowerCase();
switch (lowercasedBotType)
{
case this.pmcConfig.bearType.toLowerCase():
2023-11-10 16:49:29 -05:00
difficultySettings = this.botDifficultyHelper.getPmcDifficultySettings(
"bear",
difficulty,
this.pmcConfig.usecType,
this.pmcConfig.bearType,
);
2023-03-03 15:23:46 +00:00
break;
case this.pmcConfig.usecType.toLowerCase():
2023-11-10 16:49:29 -05:00
difficultySettings = this.botDifficultyHelper.getPmcDifficultySettings(
"usec",
difficulty,
this.pmcConfig.usecType,
this.pmcConfig.bearType,
);
2023-03-03 15:23:46 +00:00
break;
default:
difficultySettings = this.botDifficultyHelper.getBotDifficultySettings(type, difficulty);
break;
}
return difficultySettings;
}
public getAllBotDifficulties(): Record<string, any>
{
const result = {};
const botDb = this.databaseServer.getTables().bots!.types;
const botTypes = Object.keys(WildSpawnTypeNumber).filter((v) => Number.isNaN(Number(v)));
for (let botType of botTypes)
{
const enumType = botType.toLowerCase();
// sptBear/sptUsec need to be converted into `usec`/`bear` so we can read difficulty settings from bots/types
botType = this.botHelper.isBotPmc(botType)
? this.botHelper.getPmcSideByRole(botType).toLowerCase()
: botType.toLowerCase();
const botDetails = botDb[botType];
if (!botDetails?.difficulty)
{
continue;
}
const botDifficulties = Object.keys(botDetails.difficulty);
result[enumType] = {};
for (const difficulty of botDifficulties)
{
result[enumType][difficulty] = this.getBotDifficulty(enumType, difficulty, true);
}
}
return result;
}
2023-03-03 15:23:46 +00:00
/**
* Generate bot profiles and store in cache
* @param sessionId Session id
* @param info bot generation request info
* @returns IBotBase array
*/
public async generate(sessionId: string, info: IGenerateBotsRequestData): Promise<IBotBase[]>
2023-03-03 15:23:46 +00:00
{
const pmcProfile = this.profileHelper.getPmcProfile(sessionId);
2024-01-19 19:21:51 +00:00
const isFirstGen = info.conditions.length > 1;
if (isFirstGen)
{
return this.generateBotsFirstTime(info, pmcProfile, sessionId);
}
2024-01-19 19:21:51 +00:00
return this.returnSingleBotFromCache(sessionId, info);
}
/**
* On first bot generation bots are generated and stored inside a cache, ready to be used later
* @param request Bot generation request object
* @param pmcProfile Player profile
* @param sessionId Session id
* @returns
2024-01-19 19:21:51 +00:00
*/
protected async generateBotsFirstTime(
request: IGenerateBotsRequestData,
pmcProfile: IPmcData,
sessionId: string,
): Promise<IBotBase[]>
2024-01-19 19:21:51 +00:00
{
// Clear bot cache before any work starts
this.botGenerationCacheService.clearStoredBots();
const raidSettings = this.applicationContext
.getLatestValue(ContextVariableType.RAID_CONFIGURATION)
?.getValue<IGetRaidConfigurationRequestData>();
if (raidSettings === undefined)
{
2024-05-27 20:06:10 +01:00
throw new Error(this.localisationService.getText("bot-unable_to_load_raid_settings_from_appcontext"));
}
const pmcLevelRangeForMap
= this.pmcConfig.locationSpecificPmcLevelOverride[raidSettings.location.toLowerCase()];
const allPmcsHaveSameNameAsPlayer = this.randomUtil.getChance100(
this.pmcConfig.allPMCsHavePlayerNameWithRandomPrefixChance,
);
const conditionPromises: Promise<void>[] = [];
2024-01-19 19:21:51 +00:00
for (const condition of request.conditions)
2023-03-03 15:23:46 +00:00
{
const botGenerationDetails = this.getBotGenerationDetailsForWave(
condition,
pmcProfile,
allPmcsHaveSameNameAsPlayer,
pmcLevelRangeForMap,
this.botConfig.presetBatch[condition.Role],
false,
);
2023-03-03 15:23:46 +00:00
conditionPromises.push(this.generateWithBotDetails(condition, botGenerationDetails, sessionId));
}
await Promise.all(conditionPromises).then((p) => Promise.all(p));
2023-03-03 15:23:46 +00:00
return [];
}
2023-11-10 16:49:29 -05:00
/**
* Create a BotGenerationDetails for the bot generator to use
* @param condition Client data defining bot type and difficulty
* @param pmcProfile Player who is generating bots
* @param allPmcsHaveSameNameAsPlayer Should all PMCs have same name as player
* @param pmcLevelRangeForMap Min/max levels for PMCs to generate within
* @param botCountToGenerate How many bots to generate
* @param generateAsPmc Force bot being generated a PMC
* @returns BotGenerationDetails
*/
protected getBotGenerationDetailsForWave(
condition: Condition,
pmcProfile: IPmcData,
allPmcsHaveSameNameAsPlayer: boolean,
pmcLevelRangeForMap: MinMax,
botCountToGenerate: number,
generateAsPmc: boolean,
): BotGenerationDetails
{
return {
isPmc: generateAsPmc,
side: "Savage",
role: condition.Role,
playerLevel: this.getPlayerLevelFromProfile(pmcProfile),
playerName: pmcProfile.Info.Nickname,
botRelativeLevelDeltaMax: this.pmcConfig.botRelativeLevelDeltaMax,
botRelativeLevelDeltaMin: this.pmcConfig.botRelativeLevelDeltaMin,
botCountToGenerate: botCountToGenerate,
botDifficulty: condition.Difficulty,
locationSpecificPmcLevelOverride: pmcLevelRangeForMap,
isPlayerScav: false,
allPmcsHaveSameNameAsPlayer: allPmcsHaveSameNameAsPlayer,
};
}
/**
* Get players profile level
* @param pmcProfile Profile to get level from
* @returns Level as number
*/
protected getPlayerLevelFromProfile(pmcProfile: IPmcData): number
{
return pmcProfile.Info.Level;
}
/**
* Generate many bots and store then on the cache
* @param condition the condition details to generate the bots with
* @param botGenerationDetails the bot details to generate the bot with
* @param sessionId Session id
* @returns A promise for the bots to be done generating
*/
protected async generateWithBotDetails(
condition: Condition,
botGenerationDetails: BotGenerationDetails,
sessionId: string,
): Promise<void>
{
const isEventBot = condition.Role.toLowerCase().includes("event");
if (isEventBot)
{
// Add eventRole data + reassign role property to be base type
botGenerationDetails.eventRole = condition.Role;
botGenerationDetails.role = this.seasonalEventService.getBaseRoleForEventBot(
botGenerationDetails.eventRole,
);
}
// Custom map waves can have spt roles in them
// Is bot type sptusec/sptbear, set is pmc true and set side
if (this.botHelper.botRoleIsPmc(condition.Role))
{
botGenerationDetails.isPmc = true;
botGenerationDetails.side = this.botHelper.getPmcSideByRole(condition.Role);
}
// Loop over and make x bots for this bot wave
const cacheKey = `${
botGenerationDetails.eventRole ?? botGenerationDetails.role
}${botGenerationDetails.botDifficulty}`;
const botPromises: Promise<void>[] = [];
for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++)
{
const detailsClone = this.cloner.clone(botGenerationDetails);
botPromises.push(this.generateSingleBotAndStoreInCache(detailsClone, sessionId, cacheKey));
}
return Promise.all(botPromises).then(() =>
{
2024-01-19 19:21:51 +00:00
this.logger.debug(
`Generated ${botGenerationDetails.botCountToGenerate} ${botGenerationDetails.role} (${
botGenerationDetails.eventRole ?? ""
}) ${botGenerationDetails.botDifficulty} bots`,
);
});
}
2024-01-19 19:21:51 +00:00
/**
* Generate a single bot and store it in the cache
* @param botGenerationDetails the bot details to generate the bot with
* @param sessionId Session id
* @param cacheKey the cache key to store the bot with
* @returns A promise for the bot to be stored
*/
protected async generateSingleBotAndStoreInCache(
botGenerationDetails: BotGenerationDetails,
sessionId: string,
cacheKey: string,
): Promise<void>
{
const botToCache = this.botGenerator.prepareAndGenerateBot(sessionId, botGenerationDetails);
this.botGenerationCacheService.storeBots(cacheKey, [botToCache]);
2024-01-19 19:21:51 +00:00
}
/**
* Pull a single bot out of cache and return, if cache is empty add bots to it and then return
* @param sessionId Session id
* @param request Bot generation request object
* @returns Single IBotBase object
*/
protected async returnSingleBotFromCache(
sessionId: string,
request: IGenerateBotsRequestData,
): Promise<IBotBase[]>
2024-01-19 19:21:51 +00:00
{
const pmcProfile = this.profileHelper.getPmcProfile(sessionId);
const requestedBot = request.conditions[0];
const raidSettings = this.applicationContext
.getLatestValue(ContextVariableType.RAID_CONFIGURATION)
?.getValue<IGetRaidConfigurationRequestData>();
if (raidSettings === undefined)
{
2024-05-27 20:06:10 +01:00
throw new Error(this.localisationService.getText("bot-unable_to_load_raid_settings_from_appcontext"));
}
const pmcLevelRangeForMap
= this.pmcConfig.locationSpecificPmcLevelOverride[raidSettings.location.toLowerCase()];
2024-01-19 19:21:51 +00:00
// Create gen request for when cache is empty
const condition: Condition = {
Role: requestedBot.Role,
Limit: 5,
Difficulty: requestedBot.Difficulty,
2024-01-19 19:21:51 +00:00
};
const botGenerationDetails = this.getBotGenerationDetailsForWave(
condition,
pmcProfile,
false,
pmcLevelRangeForMap,
this.botConfig.presetBatch[requestedBot.Role],
false,
);
2024-01-19 19:21:51 +00:00
// 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,
);
}
if (this.botHelper.isBotPmc(botGenerationDetails.role))
{
botGenerationDetails.isPmc = true;
botGenerationDetails.side = this.botHelper.getPmcSideByRole(requestedBot.Role);
}
2024-01-19 19:21:51 +00:00
// Roll chance to be pmc if type is allowed to be one
const botConvertRateMinMax = this.pmcConfig.convertIntoPmcChance[requestedBot.Role.toLowerCase()];
if (botConvertRateMinMax)
{
// Should bot become PMC
const convertToPmc = this.botHelper.rollChanceToBePmc(requestedBot.Role, botConvertRateMinMax);
if (convertToPmc)
{
2024-01-19 19:21:51 +00:00
botGenerationDetails.isPmc = true;
botGenerationDetails.role = this.botHelper.getRandomizedPmcRole();
botGenerationDetails.side = this.botHelper.getPmcSideByRole(botGenerationDetails.role);
2024-01-19 19:21:51 +00:00
botGenerationDetails.botDifficulty = this.getPMCDifficulty(requestedBot.Difficulty);
botGenerationDetails.botCountToGenerate = this.botConfig.presetBatch[botGenerationDetails.role];
2024-01-19 19:21:51 +00:00
}
}
// Construct cache key
const cacheKey = `${
botGenerationDetails.eventRole ?? botGenerationDetails.role
}${botGenerationDetails.botDifficulty}`;
2024-01-19 19:21:51 +00:00
// Check cache for bot using above key
if (!this.botGenerationCacheService.cacheHasBotOfRole(cacheKey))
{
const botPromises: Promise<void>[] = [];
2024-01-19 19:21:51 +00:00
// No bot in cache, generate new and return one
for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++)
{
botPromises.push(this.generateSingleBotAndStoreInCache(botGenerationDetails, sessionId, cacheKey));
}
await Promise.all(botPromises).then(() =>
{
this.logger.debug(
`Generated ${botGenerationDetails.botCountToGenerate} ${botGenerationDetails.role} (${
botGenerationDetails.eventRole ?? ""
}) ${botGenerationDetails.botDifficulty} bots`,
);
});
2023-03-03 15:23:46 +00:00
}
const desiredBot = this.botGenerationCacheService.getBot(cacheKey);
this.botGenerationCacheService.storeUsedBot(desiredBot);
return [desiredBot];
2023-03-03 15:23:46 +00:00
}
/**
2023-11-10 16:49:29 -05:00
* Get the difficulty passed in, if its not "asonline", get selected difficulty from config
* @param requestedDifficulty
* @returns
2023-03-03 15:23:46 +00:00
*/
public getPMCDifficulty(requestedDifficulty: string): string
{
2023-11-10 16:49:29 -05:00
// Maybe return a random difficulty...
if (this.pmcConfig.difficulty.toLowerCase() === "asonline")
2023-03-03 15:23:46 +00:00
{
return requestedDifficulty;
}
if (this.pmcConfig.difficulty.toLowerCase() === "random")
2023-03-03 15:23:46 +00:00
{
return this.botDifficultyHelper.chooseRandomDifficulty();
}
return this.pmcConfig.difficulty;
2023-03-03 15:23:46 +00:00
}
/**
* Get the max number of bots allowed on a map
* Looks up location player is entering when getting cap value
* @returns cap number
*/
public getBotCap(): number
{
const defaultMapCapId = "default";
const raidConfig = this.applicationContext
.getLatestValue(ContextVariableType.RAID_CONFIGURATION)
?.getValue<IGetRaidConfigurationRequestData>();
2023-03-03 15:23:46 +00:00
if (!raidConfig)
{
this.logger.warning(this.localisationService.getText("bot-missing_saved_match_info"));
}
const mapName = raidConfig ? raidConfig.location : defaultMapCapId;
2023-03-03 15:23:46 +00:00
let botCap = this.botConfig.maxBotCap[mapName.toLowerCase()];
if (!botCap)
{
2023-11-10 16:49:29 -05:00
this.logger.warning(
this.localisationService.getText(
"bot-no_bot_cap_found_for_location",
raidConfig?.location.toLowerCase(),
2023-11-10 16:49:29 -05:00
),
);
2023-03-03 15:23:46 +00:00
botCap = this.botConfig.maxBotCap[defaultMapCapId];
}
return botCap;
}
public getAiBotBrainTypes(): any
2023-03-03 15:23:46 +00:00
{
return {
pmc: this.pmcConfig.pmcType,
assault: this.botConfig.assaultBrainType,
playerScav: this.botConfig.playerScavBrainType,
};
2023-03-03 15:23:46 +00:00
}
}