Files
SPT-Server-Build/Core/Controllers/BotController.cs
T
2025-01-18 13:09:28 +00:00

470 lines
18 KiB
C#

using Core.Annotations;
using Core.Context;
using Core.Generators;
using Core.Helpers;
using Core.Models.Common;
using Core.Models.Eft.Bot;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Match;
using Core.Models.Spt.Bots;
using Core.Models.Spt.Config;
using Core.Models.Utils;
using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
namespace Core.Controllers;
[Injectable]
public class BotController
{
protected ISptLogger<BotController> _logger;
protected DatabaseService _databaseService;
protected BotGenerator _botGenerator;
protected BotHelper _botHelper;
protected BotDifficultyHelper _botDifficultyHelper;
protected WeightedRandomHelper _weightedRandomHelper;
protected BotGenerationCacheService _botGenerationCacheService;
protected MatchBotDeatilsCacheService _matchBotDeatilsCacheService;
protected LocalisationService _localisationService;
protected SeasonalEventService _seasonalEventService;
protected MatchBotDetailsCacheService _matchBotDetailsCacheService;
protected ProfileHelper _profileHelper;
protected ConfigServer _configServer;
protected ApplicationContext _applicationContext;
protected RandomUtil _randomUtil;
protected ICloner _cloner;
protected BotConfig _botConfig;
protected PmcConfig _pmcConfig;
public BotController
(
ISptLogger<BotController> logger,
DatabaseService databaseService,
BotGenerator botGenerator,
BotHelper botHelper,
BotDifficultyHelper botDifficultyHelper,
WeightedRandomHelper weightedRandomHelper,
BotGenerationCacheService botGenerationCacheService,
MatchBotDeatilsCacheService matchBotDeatilsCacheService,
LocalisationService localisationService,
SeasonalEventService seasonalEventService,
MatchBotDetailsCacheService matchBotDetailsCacheService,
ProfileHelper profileHelper,
ConfigServer configServer,
ApplicationContext applicationContext,
RandomUtil randomUtil,
ICloner cloner
)
{
_logger = logger;
_databaseService = databaseService;
_botGenerator = botGenerator;
_botHelper = botHelper;
_botDifficultyHelper = botDifficultyHelper;
_weightedRandomHelper = weightedRandomHelper;
_botGenerationCacheService = botGenerationCacheService;
_matchBotDeatilsCacheService = matchBotDeatilsCacheService;
_localisationService = localisationService;
_seasonalEventService = seasonalEventService;
_matchBotDetailsCacheService = matchBotDetailsCacheService;
_profileHelper = profileHelper;
_configServer = configServer;
_applicationContext = applicationContext;
_randomUtil = randomUtil;
_cloner = cloner;
_botConfig = _configServer.GetConfig<BotConfig>();
_pmcConfig = _configServer.GetConfig<PmcConfig>();
}
public int GetBotPresetGenerationLimit(string type)
{
var typeInLower = type.ToLower();
var value = (int)typeof(PresetBatch).GetProperties().First(p => p.Name.ToLower() == (typeInLower == "assaultgroup" ? "assault" : typeInLower))
.GetValue(_botConfig.PresetBatch);
if (value == null)
{
_logger.Warning(_localisationService.GetText("bot-bot_preset_count_value_missing", type));
return 30;
}
return value;
}
public Dictionary<string, object> GetBotCoreDifficulty()
{
return _databaseService.GetBots().Core;
}
public DifficultyCategories GetBotDifficulty(string type, string diffLevel, GetRaidConfigurationRequestData? raidConfig, bool ignoreRaidSettings = false)
{
var difficulty = diffLevel.ToLower();
if (!(raidConfig != null || ignoreRaidSettings))
_logger.Error(_localisationService.GetText("bot-missing_application_context", "RAID_CONFIGURATION"));
// Check value chosen in pre-raid difficulty dropdown
// If value is not 'asonline', change requested difficulty to be what was chosen in dropdown
var botDifficultyDropDownValue = raidConfig?.WavesSettings?.BotDifficulty?.ToString().ToLower() ?? "asonline";
if (botDifficultyDropDownValue != "asonline")
difficulty = _botDifficultyHelper.ConvertBotDifficultyDropdownToBotDifficulty(botDifficultyDropDownValue);
var botDb = _databaseService.GetBots();
return _botDifficultyHelper.GetBotDifficultySettings(type, difficulty, botDb);
}
public Dictionary<string, Dictionary<string, DifficultyCategories>> GetAllBotDifficulties()
{
var result = new Dictionary<string, Dictionary<string, DifficultyCategories>>();
var botTypesDb = _databaseService.GetBots().Types;
//Get all bot types as sting array
var botTypes = Enum.GetValues<WildSpawnType>().Select(item => item.ToString()).ToList();
foreach (var botType in botTypes)
{
// If bot is usec/bear, swap to different name
var botTypeLower = _botHelper.IsBotPmc(botType)
? _botHelper.GetPmcSideByRole(botType).ToLower()
: botType.ToLower();
// Get details from db
if (!botTypesDb.TryGetValue(botTypeLower, out var botDetails))
{
// No bot of this type found, skip
continue;
};
if (botDetails.BotDifficulty is null)
{
// Bot has no difficulty values, skip
continue;
}
var botNameKey = botType.ToLower();
foreach (var (difficultyName, difficultyValues) in botDetails.BotDifficulty)
{
// Bot doesnt exist in result, add
if (!result.ContainsKey(botNameKey))
{
result.TryAdd(botNameKey, new Dictionary<string, DifficultyCategories>());
}
// Store all difficulty values in dict keyed by difficulty type e.g. easy/normal/impossible
result[botNameKey].Add(difficultyName, GetBotDifficulty(botNameKey, difficultyName, null, true));
}
}
return result;
}
public List<BotBase> Generate(string sessionId, GenerateBotsRequestData info)
{
var pmcProfile = _profileHelper.GetPmcProfile(sessionId);
// Use this opportunity to create and cache bots for later retrieval
var multipleBotTypesRequested = info.Conditions.Count > 1;
if (multipleBotTypesRequested)
{
return GenerateMultipleBotsAndCache(info, pmcProfile, sessionId);
}
return ReturnSingleBotFromCache(sessionId, info);
}
public List<BotBase> GenerateMultipleBotsAndCache(GenerateBotsRequestData request, PmcData pmcProfile, string sessionId)
{
var raidSettings = GetMostRecentRaidSettings();
var allPmcsHaveSameNameAsPlayer = _randomUtil.GetChance100(
_pmcConfig.AllPMCsHavePlayerNameWithRandomPrefixChance);
// Map conditions to promises for bot generation
foreach (var condition in request.Conditions)
{
var botGenerationDetails = GetBotGenerationDetailsForWave(
condition,
pmcProfile,
allPmcsHaveSameNameAsPlayer,
raidSettings,
_botConfig.PresetBatch.GetValueOrDefault(condition.Role, 15),
_botHelper.IsBotPmc(condition.Role));
// Generate bots for the current condition
GenerateWithBotDetails(condition, botGenerationDetails, sessionId);
}
return [];
}
private void GenerateWithBotDetails(GenerateCondition condition, BotGenerationDetails botGenerationDetails, string sessionId)
{
var isEventBot = condition.Role.ToLower().Contains("event");
if (isEventBot)
{
// Add eventRole data + reassign role property to be base type
botGenerationDetails.EventRole = condition.Role;
botGenerationDetails.Role = _seasonalEventService.GetBaseRoleForEventBot(
botGenerationDetails.EventRole);
}
// Create a compound key to store bots in cache against
var cacheKey = _botGenerationCacheService.CreateCacheKey(
botGenerationDetails.EventRole ?? botGenerationDetails.Role,
botGenerationDetails.BotDifficulty);
// Get number of bots we have in cache
var botCacheCount = _botGenerationCacheService.GetCachedBotCount(cacheKey);
if (botCacheCount >= botGenerationDetails.BotCountToGenerate)
{
_logger.Debug($"Cache already has sufficient bots: { botCacheCount}");
return;
}
// We're below desired count, add bots to cache
var botsToGenerate = botGenerationDetails.BotCountToGenerate - botCacheCount;
var progressWriter = new ProgressWriter(botGenerationDetails.BotCountToGenerate.Value);
_logger.Debug($"Generating { botsToGenerate} bots for cacheKey: {cacheKey}");
for (var i = 0; i < botsToGenerate; i++)
{
try
{
var detailsClone = _cloner.Clone(botGenerationDetails);
GenerateSingleBotAndStoreInCache(detailsClone, sessionId, cacheKey);
progressWriter.Increment();
}
catch (Exception e)
{
_logger.Error($"Failed to generate bot #{i + 1}: {e.Message}");
}
}
_logger.Debug($"Generated { botGenerationDetails.BotCountToGenerate} { botGenerationDetails.Role}" +
$"({botGenerationDetails.EventRole ?? botGenerationDetails.Role ?? ""}) { botGenerationDetails.BotDifficulty}bots");
}
private List<BotBase> ReturnSingleBotFromCache(string sessionId, GenerateBotsRequestData request)
{
var pmcProfile = _profileHelper.GetPmcProfile(sessionId);
var requestedBot = request.Conditions[0];
var raidSettings = GetMostRecentRaidSettings();
// Create generation request for when cache is empty
var condition = new GenerateCondition{
Role = requestedBot.Role,
Limit= 5,
Difficulty= requestedBot.Difficulty,
};
var botGenerationDetails = GetBotGenerationDetailsForWave(
condition,
pmcProfile,
false,
raidSettings,
_botConfig.PresetBatch[requestedBot.Role],
_botHelper.IsBotPmc(requestedBot.Role)
);
// Event bots need special actions to occur, set data up for them
var isEventBot = requestedBot.Role.ToLower().Contains("event");
if (isEventBot)
{
// Add eventRole data + reassign role property
botGenerationDetails.EventRole = requestedBot.Role;
botGenerationDetails.Role = _seasonalEventService.GetBaseRoleForEventBot(
botGenerationDetails.EventRole);
}
// Does non pmc bot have a chance of being converted into a pmc
var convertIntoPmcChanceMinMax = GetPmcConversionMinMaxForLocation(
requestedBot.Role,
raidSettings?.Location);
if (convertIntoPmcChanceMinMax is not null && !botGenerationDetails.IsPmc.Value)
{
// Bot has % chance to become pmc and isnt one pmc already
var convertToPmc = _botHelper.RollChanceToBePmc(convertIntoPmcChanceMinMax);
if (convertToPmc)
{
// Update requirements
botGenerationDetails.IsPmc = true;
botGenerationDetails.Role = _botHelper.GetRandomizedPmcRole();
botGenerationDetails.Side = _botHelper.GetPmcSideByRole(botGenerationDetails.Role);
botGenerationDetails.BotDifficulty = GetPmcDifficulty(requestedBot.Difficulty);
botGenerationDetails.BotCountToGenerate = _botConfig.PresetBatch[botGenerationDetails.Role];
}
}
// Only convert to boss when not already converted to PMC & Boss Convert is enabled
var bossConvertEnabled = _botConfig.AssaultToBossConversion.BossConvertEnabled;
var bossConvertMinMax = _botConfig.AssaultToBossConversion.BossConvertMinMax;
var bossesToConvertToWeights = _botConfig.AssaultToBossConversion.BossesToConvertToWeights;
if (bossConvertEnabled && !botGenerationDetails.IsPmc.Value)
{
var bossConvertPercent = bossConvertMinMax[requestedBot.Role.ToLower()];
if (bossConvertPercent is not null)
{
// Roll a percentage check if we should convert scav to boss
if (
_randomUtil.GetChance100(_randomUtil.GetDouble(bossConvertPercent.Min.Value, bossConvertPercent.Max.Value))
)
{
UpdateBotGenerationDetailsToRandomBoss(botGenerationDetails, bossesToConvertToWeights);
}
}
}
// Create a compound key to store bots in cache against
var cacheKey = _botGenerationCacheService.CreateCacheKey(
botGenerationDetails.EventRole ?? botGenerationDetails.Role,
botGenerationDetails.BotDifficulty);
// Check cache for bot using above key
if (!_botGenerationCacheService.CacheHasBotWithKey(cacheKey))
{
// No bot in cache, generate new and store in cache
GenerateSingleBotAndStoreInCache(botGenerationDetails, sessionId, cacheKey);
_logger.Debug($"Generated { botGenerationDetails.BotCountToGenerate} " +
$"{ botGenerationDetails.Role} ({botGenerationDetails.EventRole ?? ""}) {botGenerationDetails.BotDifficulty} bots");
}
var desiredBot = _botGenerationCacheService.GetBot(cacheKey);
_botGenerationCacheService.StoreUsedBot(desiredBot);
return [desiredBot];
}
private void GenerateSingleBotAndStoreInCache(BotGenerationDetails botGenerationDetails, string sessionId, string cacheKey)
{
var botToCache = _botGenerator.PrepareAndGenerateBot(sessionId, botGenerationDetails);
_botGenerationCacheService.StoreBots(cacheKey, [botToCache]);
// Store bot details in cache so post-raid PMC messages can use data
_matchBotDetailsCacheService.CacheBot(botToCache);
}
private void UpdateBotGenerationDetailsToRandomBoss(BotGenerationDetails botGenerationDetails, Dictionary<string, double> bossesToConvertToWeights)
{
// Seems Actual bosses have the same Brain issues like PMC gaining Boss Brains We can't use all bosses
botGenerationDetails.Role = _weightedRandomHelper.GetWeightedValue(bossesToConvertToWeights);
// Bosses are only ever 'normal'
botGenerationDetails.BotDifficulty = "normal";
botGenerationDetails.BotCountToGenerate = _botConfig.PresetBatch[botGenerationDetails.Role];
}
private string GetPmcDifficulty(string requestedBotDifficulty)
{
var difficulty = _pmcConfig.Difficulty.ToLower();
return difficulty switch
{
"asonline" => requestedBotDifficulty,
"random" => _botDifficultyHelper.ChooseRandomDifficulty(),
_ => _pmcConfig.Difficulty
};
}
private MinMax GetPmcConversionMinMaxForLocation(string requestedBotRole, string location)
{
var mapSpecificConversionValues = _pmcConfig.ConvertIntoPmcChance.GetValueOrDefault(location.ToLower(), null);
if (mapSpecificConversionValues is null)
{
return _pmcConfig.ConvertIntoPmcChance["default"][requestedBotRole];
}
return mapSpecificConversionValues[requestedBotRole.ToLower()];
}
public GetRaidConfigurationRequestData GetMostRecentRaidSettings()
{
var raidSettings = _applicationContext
.GetLatestValue(ContextVariableType.RAID_CONFIGURATION)
.GetValue<GetRaidConfigurationRequestData>();
if (raidSettings is null)
{
_logger.Warning(_localisationService.GetText("bot-unable_to_load_raid_settings_from_appcontext"));
}
return raidSettings;
}
public MinMax GetPmcLevelRangeForMap(string location)
{
return _pmcConfig.LocationSpecificPmcLevelOverride.GetValueOrDefault(location.ToLower(), null);
}
public BotGenerationDetails GetBotGenerationDetailsForWave(
GenerateCondition condition,
PmcData pmcProfile,
bool allPmcsHaveSameNameAsPlayer,
GetRaidConfigurationRequestData raidSettings,
int botCountToGenerate,
bool generateAsPmc)
{
return new BotGenerationDetails{
IsPmc = generateAsPmc,
Side = generateAsPmc ? _botHelper.GetPmcSideByRole(condition.Role) : "Savage",
Role = condition.Role,
PlayerLevel = pmcProfile.Info.Level.Value,
PlayerName = pmcProfile.Info.Nickname,
BotRelativeLevelDeltaMax = _pmcConfig.BotRelativeLevelDeltaMax,
BotRelativeLevelDeltaMin = _pmcConfig.BotRelativeLevelDeltaMin,
BotCountToGenerate = botCountToGenerate,
BotDifficulty = condition.Difficulty,
LocationSpecificPmcLevelOverride = this.GetPmcLevelRangeForMap(raidSettings?.Location), // Min/max levels for PMCs to generate within
IsPlayerScav = false,
AllPmcsHaveSameNameAsPlayer = allPmcsHaveSameNameAsPlayer,
};
}
public int GetBotLimit(string type)
{
throw new NotImplementedException();
}
public bool IsBotPmc(string botRole)
{
throw new NotImplementedException();
}
public bool IsBotBoss(string botRole)
{
throw new NotImplementedException();
}
public bool IsBotFollower(string botRole)
{
throw new NotImplementedException();
}
public int GetBotCap(string location)
{
var botCap = _botConfig.MaxBotCap[location.ToLower()];
if (location == "default")
{
_logger.Warning(
_localisationService.GetText("bot-no_bot_cap_found_for_location", location.ToLower()));
}
return botCap;
}
public object GetAiBotBrainTypes()
{
// TODO: Returns `any` in the node server
return new {
pmc = _pmcConfig.PmcType,
assault = _botConfig.AssaultBrainType,
playerScav = _botConfig.PlayerScavBrainType,
};
}
}