Improved how bots are generated to be less blocking
Updated `GenerateBotWaves` to return empty results when request data is empty Cleaned up `GenerateBotWave` to not need a manual lock Improved `botRelativeLevelDelta` value in pmc.config Updated `MatchBotDetailsCacheService` to store PMCs primary weapon tpl
This commit is contained in:
@@ -167,8 +167,10 @@
|
||||
],
|
||||
"useDifficultyOverride": false,
|
||||
"difficulty": "AsOnline",
|
||||
"botRelativeLevelDeltaMax": 10,
|
||||
"botRelativeLevelDeltaMin": 70,
|
||||
"botRelativeLevelDelta": {
|
||||
"min": 70,
|
||||
"max": 10
|
||||
},
|
||||
"isUsec": 60,
|
||||
"_pmcType": "Controls what bot brain can be chosen for each PMC bot type, the number is the weighting to be picked",
|
||||
"pmcType": {
|
||||
|
||||
@@ -52,9 +52,9 @@ public class BotCallbacks(BotController botController, HttpResponseUtil httpResp
|
||||
/// Handle client/game/bot/generate
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ValueTask<string> GenerateBots(string url, GenerateBotsRequestData info, MongoId sessionID)
|
||||
public async ValueTask<string> GenerateBots(string url, GenerateBotsRequestData info, MongoId sessionID)
|
||||
{
|
||||
return new ValueTask<string>(httpResponseUtil.GetBody(botController.Generate(sessionID, info)));
|
||||
return httpResponseUtil.GetBody(await botController.Generate(sessionID, info));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -166,70 +166,73 @@ public class BotController(
|
||||
/// <param name="sessionId">Session/Player id</param>
|
||||
/// <param name="request"></param>
|
||||
/// <returns>List of bots</returns>
|
||||
public List<BotBase> Generate(MongoId sessionId, GenerateBotsRequestData request)
|
||||
public async Task<IEnumerable<BotBase>> Generate(MongoId sessionId, GenerateBotsRequestData request)
|
||||
{
|
||||
var pmcProfile = _profileHelper.GetPmcProfile(sessionId);
|
||||
|
||||
return GenerateBotWaves(request, pmcProfile, sessionId);
|
||||
return await GenerateBotWaves(sessionId, request, pmcProfile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate bots for passed in wave data
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="pmcProfile">Player generating bots</param>
|
||||
/// <param name="sessionId">Session/Player id</param>
|
||||
/// <param name="request">Client bot generation request</param>
|
||||
/// <param name="pmcProfile">Player profile generating bots</param>
|
||||
/// <returns>List of generated bots</returns>
|
||||
protected List<BotBase> GenerateBotWaves(GenerateBotsRequestData request, PmcData? pmcProfile, MongoId sessionId)
|
||||
protected async Task<IEnumerable<BotBase>> GenerateBotWaves(MongoId sessionId, GenerateBotsRequestData request, PmcData? pmcProfile)
|
||||
{
|
||||
var generatedBotList = new List<BotBase>();
|
||||
if (request.Conditions is null || !request.Conditions.Any())
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
// Get chosen raid settings from app context
|
||||
var raidSettings = GetMostRecentRaidSettings(sessionId);
|
||||
var allPmcsHaveSameNameAsPlayer = _randomUtil.GetChance100(_pmcConfig.AllPMCsHavePlayerNameWithRandomPrefixChance);
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
// Map conditions to promises for bot generation
|
||||
// Split each bot wave into its own task
|
||||
var waveGenerationTasks = request.Conditions.Select(condition =>
|
||||
Task.Run(() =>
|
||||
{
|
||||
var botWaveGenerationDetails = GetBotGenerationDetailsForWave(
|
||||
condition,
|
||||
pmcProfile,
|
||||
allPmcsHaveSameNameAsPlayer,
|
||||
raidSettings
|
||||
);
|
||||
|
||||
Task.WaitAll(
|
||||
(request.Conditions ?? [])
|
||||
.Select(condition =>
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
var botWaveGenerationDetails = GetBotGenerationDetailsForWave(
|
||||
condition,
|
||||
pmcProfile,
|
||||
allPmcsHaveSameNameAsPlayer,
|
||||
raidSettings
|
||||
);
|
||||
|
||||
GenerateBotWave(condition, botWaveGenerationDetails, generatedBotList, sessionId);
|
||||
})
|
||||
)
|
||||
.ToArray()
|
||||
// Add bot wave results directly to `botsInWave`
|
||||
return GenerateBotWave(sessionId, condition, botWaveGenerationDetails);
|
||||
})
|
||||
);
|
||||
|
||||
stopwatch.Stop();
|
||||
// Wait for all above tasks to complete
|
||||
var results = await Task.WhenAll(waveGenerationTasks);
|
||||
|
||||
stopwatch.Stop();
|
||||
if (_logger.IsLogEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.Debug($"Took {stopwatch.ElapsedMilliseconds}ms to GenerateMultipleBotsAndCache()");
|
||||
}
|
||||
|
||||
return generatedBotList;
|
||||
// Merge + flatten results of all wave generations
|
||||
return results.SelectMany(botList => botList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate bots for a single wave request
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session/Player id</param>
|
||||
/// <param name="generateRequest"></param>
|
||||
/// <param name="botGenerationDetails"></param>
|
||||
/// <param name="botList">List of bots to fill</param>
|
||||
/// <param name="sessionId">Session/Player id</param>
|
||||
/// <returns></returns>
|
||||
protected void GenerateBotWave(
|
||||
/// <returns>Result of generating bot wave</returns>
|
||||
protected IEnumerable<BotBase> GenerateBotWave(
|
||||
MongoId sessionId,
|
||||
GenerateCondition generateRequest,
|
||||
BotGenerationDetails botGenerationDetails,
|
||||
List<BotBase> botList,
|
||||
MongoId sessionId
|
||||
BotGenerationDetails botGenerationDetails
|
||||
)
|
||||
{
|
||||
var isEventBot = generateRequest.Role?.Contains("event", StringComparison.OrdinalIgnoreCase);
|
||||
@@ -240,6 +243,7 @@ public class BotController(
|
||||
botGenerationDetails.Role = _seasonalEventService.GetBaseRoleForEventBot(botGenerationDetails.EventRole);
|
||||
}
|
||||
|
||||
// Event role must take priority to generate correctly
|
||||
var role = botGenerationDetails.EventRole ?? botGenerationDetails.Role;
|
||||
|
||||
if (_logger.IsLogEnabled(LogLevel.Debug))
|
||||
@@ -249,46 +253,14 @@ public class BotController(
|
||||
);
|
||||
}
|
||||
|
||||
var maxThreads = botGenerationDetails.BotCountToGenerate;
|
||||
|
||||
#if DEBUG
|
||||
// Make debugging bot gen easier
|
||||
maxThreads = 1;
|
||||
#endif
|
||||
|
||||
Parallel.For(
|
||||
0,
|
||||
maxThreads,
|
||||
(i) =>
|
||||
{
|
||||
BotBase bot = null;
|
||||
|
||||
try
|
||||
{
|
||||
bot = _botGenerator.PrepareAndGenerateBot(sessionId, _cloner.Clone(botGenerationDetails));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error($"Failed to generate bot: {botGenerationDetails.Role} #{i + 1}: {e.Message} {e.StackTrace}");
|
||||
return;
|
||||
}
|
||||
|
||||
// The client expects the Side for PMCs to be `Savage`
|
||||
// We do this here so it's after we cache the bot in the match details lookup, as when you die, they will have the right side
|
||||
if (bot.Info.Side is Sides.Bear or Sides.Usec)
|
||||
{
|
||||
bot.Info.Side = Sides.Savage;
|
||||
}
|
||||
|
||||
lock (_botListLock)
|
||||
{
|
||||
botList.Add(bot);
|
||||
}
|
||||
|
||||
// Store bot details in cache so post-raid PMC messages can use data
|
||||
_matchBotDetailsCacheService.CacheBot(bot);
|
||||
}
|
||||
);
|
||||
var generatedBots = Enumerable
|
||||
.Range(0, botGenerationDetails.BotCountToGenerate)
|
||||
.AsParallel() // Parallelise above range of values so they can each generate a bot
|
||||
.Select(i => TryGenerateSingleBot(sessionId, botGenerationDetails, i))
|
||||
.Where(bot =>
|
||||
bot is not null
|
||||
) // Skip failed bots
|
||||
; // Materialise parallel query into data
|
||||
|
||||
if (_logger.IsLogEnabled(LogLevel.Debug))
|
||||
{
|
||||
@@ -297,6 +269,37 @@ public class BotController(
|
||||
+ $"({botGenerationDetails.EventRole ?? botGenerationDetails.Role ?? ""}) {botGenerationDetails.BotDifficulty} bots"
|
||||
);
|
||||
}
|
||||
|
||||
return generatedBots;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to generate and cache a single bot
|
||||
/// </summary>
|
||||
/// <returns>BotBase object or null.</returns>
|
||||
protected BotBase? TryGenerateSingleBot(MongoId sessionId, BotGenerationDetails generationDetails, int botIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Clone for thread safety TODO: confirm if clone is necessary (likely not)
|
||||
var bot = _botGenerator.PrepareAndGenerateBot(sessionId, _cloner.Clone(generationDetails));
|
||||
|
||||
// Client expects Side for PMCs to be `Savage`, must be altered here before it's cached
|
||||
if (bot.Info.Side is Sides.Bear or Sides.Usec)
|
||||
{
|
||||
bot.Info.Side = Sides.Savage;
|
||||
}
|
||||
|
||||
// Store bot details in cache before returning.
|
||||
_matchBotDetailsCacheService.CacheBot(bot);
|
||||
|
||||
return bot;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error($"Failed to generate bot #{botIndex + 1} ({generationDetails.Role}): {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -349,8 +352,8 @@ public class BotController(
|
||||
Role = condition.Role,
|
||||
PlayerLevel = pmcProfile?.Info?.Level ?? 1,
|
||||
PlayerName = pmcProfile?.Info?.Nickname,
|
||||
BotRelativeLevelDeltaMax = _pmcConfig.BotRelativeLevelDeltaMax,
|
||||
BotRelativeLevelDeltaMin = _pmcConfig.BotRelativeLevelDeltaMin,
|
||||
BotRelativeLevelDeltaMax = _pmcConfig.BotRelativeLevelDelta.Max,
|
||||
BotRelativeLevelDeltaMin = _pmcConfig.BotRelativeLevelDelta.Min,
|
||||
BotCountToGenerate = Math.Max(GetBotPresetGenerationLimit(condition.Role), condition.Limit), // Choose largest between value passed in from request vs what's in bot.config
|
||||
BotDifficulty = condition.Difficulty,
|
||||
LocationSpecificPmcLevelOverride = GetPmcLevelRangeForMap(raidSettings?.Location), // Min/max levels for PMCs to generate within
|
||||
|
||||
@@ -230,6 +230,7 @@ public static class ItemExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: return IEnumerable and update all calling code
|
||||
/// Get an item with its attachments (children)
|
||||
/// </summary>
|
||||
/// <param name="items">List of items (item + possible children)</param>
|
||||
|
||||
@@ -115,7 +115,7 @@ public class BotGenerator(
|
||||
/// <returns>constructed bot</returns>
|
||||
public BotBase PrepareAndGenerateBot(MongoId sessionId, BotGenerationDetails botGenerationDetails)
|
||||
{
|
||||
var preparedBotBase = GetPreparedBotBase(
|
||||
var botBaseClone = GetPreparedBotBaseClone(
|
||||
botGenerationDetails.EventRole ?? botGenerationDetails.Role, // Use eventRole if provided
|
||||
botGenerationDetails.Side,
|
||||
botGenerationDetails.BotDifficulty
|
||||
@@ -123,7 +123,7 @@ public class BotGenerator(
|
||||
|
||||
// Get raw json data for bot (Cloned)
|
||||
var botRole = botGenerationDetails.IsPmc
|
||||
? preparedBotBase.Info.Side // Use side to get usec.json or bear.json when bot will be PMC
|
||||
? botBaseClone.Info.Side // Use side to get usec.json or bear.json when bot will be PMC
|
||||
: botGenerationDetails.Role;
|
||||
var botJsonTemplateClone = cloner.Clone(botHelper.GetBotTemplate(botRole));
|
||||
if (botJsonTemplateClone is null)
|
||||
@@ -131,7 +131,7 @@ public class BotGenerator(
|
||||
logger.Error($"Unable to retrieve: {botRole} bot template, cannot generate bot of this type");
|
||||
}
|
||||
|
||||
return GenerateBot(sessionId, preparedBotBase, botJsonTemplateClone, botGenerationDetails);
|
||||
return GenerateBot(sessionId, botBaseClone, botJsonTemplateClone, botGenerationDetails);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -141,7 +141,7 @@ public class BotGenerator(
|
||||
/// <param name="botSide">Side bot should have</param>
|
||||
/// <param name="difficulty">Difficult bot should have</param>
|
||||
/// <returns>Cloned bot base</returns>
|
||||
protected BotBase GetPreparedBotBase(string botRole, string botSide, string difficulty)
|
||||
protected BotBase GetPreparedBotBaseClone(string botRole, string botSide, string difficulty)
|
||||
{
|
||||
var botBaseClone = GetBotBaseClone();
|
||||
botBaseClone.Info.Settings.Role = botRole;
|
||||
@@ -186,7 +186,7 @@ public class BotGenerator(
|
||||
_botConfig.BotRolesThatMustHaveUniqueName
|
||||
);
|
||||
|
||||
// Only Pmcs should have a lower nickname
|
||||
// Only PMCs need a lower nickname
|
||||
bot.Info.LowerNickname = botGenerationDetails.IsPmc ? bot.Info.Nickname.ToLowerInvariant() : string.Empty;
|
||||
|
||||
// Only run when generating a 'fake' playerscav, not actual player scav
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
|
||||
namespace SPTarkov.Server.Core.Models.Spt.Bots;
|
||||
@@ -17,4 +18,5 @@ public record BotDetailsForChatMessages
|
||||
public int? Level { get; set; }
|
||||
|
||||
public MemberCategory? Type { get; set; }
|
||||
public MongoId? PrimaryWeapon { get; set; }
|
||||
}
|
||||
|
||||
@@ -106,14 +106,8 @@ public record PmcConfig : BaseConfig
|
||||
/// <summary>
|
||||
/// How many levels above player level can a PMC be
|
||||
/// </summary>
|
||||
[JsonPropertyName("botRelativeLevelDeltaMax")]
|
||||
public required int BotRelativeLevelDeltaMax { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How many levels below player level can a PMC be
|
||||
/// </summary>
|
||||
[JsonPropertyName("botRelativeLevelDeltaMin")]
|
||||
public required int BotRelativeLevelDeltaMin { get; set; }
|
||||
[JsonPropertyName("botRelativeLevelDelta")]
|
||||
public required MinMax<int> BotRelativeLevelDelta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Force a number of healing items into PMCs secure container to ensure they can heal
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Constants;
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
using SPTarkov.Server.Core.Models.Spt.Bots;
|
||||
@@ -15,9 +16,9 @@ namespace SPTarkov.Server.Core.Services;
|
||||
[Injectable(InjectionType.Singleton)]
|
||||
public class MatchBotDetailsCacheService(ISptLogger<MatchBotDetailsCacheService> logger)
|
||||
{
|
||||
private static readonly FrozenSet<string> _sidesToCache = [Sides.PmcUsec, Sides.PmcBear];
|
||||
private static readonly FrozenSet<string> _rolesToCache = [Sides.PmcUsec, Sides.PmcBear];
|
||||
|
||||
protected readonly ConcurrentDictionary<string, BotDetailsForChatMessages> BotDetailsCache = new();
|
||||
protected readonly ConcurrentDictionary<MongoId, BotDetailsForChatMessages> BotDetailsCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Store a bot in the cache, keyed by its ID.
|
||||
@@ -36,21 +37,22 @@ public class MatchBotDetailsCacheService(ISptLogger<MatchBotDetailsCacheService>
|
||||
return;
|
||||
}
|
||||
|
||||
// If bot isn't a PMC, skip
|
||||
if (botToCache.Info?.Settings?.Role is null || !_sidesToCache.Contains(botToCache.Info.Settings.Role))
|
||||
// ignore bot when not in role whitelist
|
||||
if (botToCache.Info?.Settings?.Role is null || !_rolesToCache.Contains(botToCache.Info.Settings.Role))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BotDetailsCache.TryAdd(
|
||||
botToCache.Id,
|
||||
new BotDetailsForChatMessages()
|
||||
botToCache.Id.Value,
|
||||
new BotDetailsForChatMessages
|
||||
{
|
||||
Nickname = botToCache.Info.Nickname.Trim(),
|
||||
Side = botToCache.Info.Side == Sides.PmcUsec ? DogtagSide.Usec : DogtagSide.Bear,
|
||||
Aid = botToCache.Aid,
|
||||
Type = botToCache.Info.MemberCategory,
|
||||
Level = botToCache.Info.Level,
|
||||
PrimaryWeapon = botToCache.Inventory.Items.FirstOrDefault(x => x.SlotId == "FirstPrimaryWeapon")?.Template,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -68,17 +70,18 @@ public class MatchBotDetailsCacheService(ISptLogger<MatchBotDetailsCacheService>
|
||||
/// </summary>
|
||||
/// <param name="id"> ID of bot to find </param>
|
||||
/// <returns></returns>
|
||||
public BotDetailsForChatMessages? GetBotById(string? id)
|
||||
public BotDetailsForChatMessages? GetBotById(MongoId? id)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var botInCache = BotDetailsCache.GetValueOrDefault(id, null);
|
||||
var botInCache = BotDetailsCache.GetValueOrDefault(id.Value, null);
|
||||
if (botInCache is null)
|
||||
{
|
||||
logger.Warning($"Bot not found in match bot cache: {id}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user