Bot generation performance (#231)

* chore: Bot generation performance improvements (#227)

* Bot generation performance improvements

* Revert debug logging logic

* Get rid of Info log which is bound to be printed inevitably at each bot wave generation

* Use assault const

* Removed comments

Removed lowercase side constants
Renamed equipment role to better explain its purpose

Fixed `Blocks` property being serialised to client

Updated `_slotsWithNoCompatIssues` to use existing enum type

Reduced log line to be debug

* Fixed `GetRandomTextThatMatchesPartialKey` incorrectly checking eft locales instead of spt locales

* Updated various methods to be protected

---------

Co-authored-by: hulkhan22 <clandestine984@gmail.com>
Co-authored-by: Chomp <dev@dev.sp-tarkov.com>
This commit is contained in:
Chomp
2025-05-04 12:51:45 +01:00
committed by GitHub
parent 0019738a8d
commit 31ae5feb41
13 changed files with 212 additions and 109 deletions
@@ -0,0 +1,13 @@
namespace SPTarkov.Server.Core.Constants;
public static class Containers
{
public const string ArmorVest = "ArmorVest";
public const string Earpiece = "Earpiece";
public const string FaceCover = "FaceCover";
public const string Headwear = "Headwear";
public const string Eyewear = "Eyewear";
public const string Collapsible = "Collapsible";
public const string LeftStance = "LeftStance";
public const string Folding = "Folding";
}
@@ -0,0 +1,6 @@
namespace SPTarkov.Server.Core.Constants;
public class Roles
{
public const string Assault = "assault";
}
@@ -0,0 +1,12 @@
namespace SPTarkov.Server.Core.Constants;
public static class Sides
{
public const string Usec = "Usec";
public const string Bear = "Bear";
public const string Savage = "Savage";
public const string PmcUsec = "pmcUSEC";
public const string PmcBear = "pmcBEAR";
public const string PmcEquipmentRole = "pmc";
}
@@ -0,0 +1,6 @@
namespace SPTarkov.Server.Core.Constants;
public static class Slots
{
public const string Dogtag = "Dogtag";
}
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Text.Json.Serialization;
using SPTarkov.Common.Annotations;
using SPTarkov.Server.Core.Constants;
using SPTarkov.Server.Core.Context;
using SPTarkov.Server.Core.Generators;
using SPTarkov.Server.Core.Helpers;
@@ -125,10 +126,10 @@ public class BotController(
if (!botTypesDb.TryGetValue(botTypeLower, out var botDetails))
{
// No bot of this type found, copy details from assault
result[botTypeLower] = result["assault"];
result[botTypeLower] = result[Roles.Assault];
if (_logger.IsLogEnabled(LogLevel.Debug))
{
_logger.Debug($"Unable to find bot: {botTypeLower} in db, copying 'assault'");
_logger.Debug($"Unable to find bot: {botTypeLower} in db, copying '{Roles.Assault}'");
}
continue;
@@ -180,13 +181,12 @@ public class BotController(
/// <returns>List of generated bots</returns>
protected List<BotBase> GenerateBotWaves(GenerateBotsRequestData request, PmcData? pmcProfile, string sessionId)
{
var result = new List<BotBase>();
var generatedBotList = new List<BotBase>();
var raidSettings = GetMostRecentRaidSettings();
var allPmcsHaveSameNameAsPlayer = _randomUtil.GetChance100(
_pmcConfig.AllPMCsHavePlayerNameWithRandomPrefixChance
);
var stopwatch = Stopwatch.StartNew();
// Map conditions to promises for bot generation
@@ -197,24 +197,19 @@ public class BotController(
condition,
pmcProfile,
allPmcsHaveSameNameAsPlayer,
raidSettings,
Math.Max(GetBotPresetGenerationLimit(condition.Role),
condition.Limit), // Choose largest between value passed in from request vs what's in bot.config
_botHelper.IsBotPmc(condition.Role));
raidSettings);
lock (_botListLock)
{
result.AddRange(GenerateBotWave(condition, botWaveGenerationDetails, sessionId));
}
GenerateBotWave(condition, botWaveGenerationDetails, generatedBotList, sessionId);
})).ToArray());
stopwatch.Stop();
if (_logger.IsLogEnabled(LogLevel.Debug))
{
_logger.Debug($"Took {stopwatch.ElapsedMilliseconds}ms to GenerateMultipleBotsAndCache()");
}
return result;
return generatedBotList;
}
/// <summary>
@@ -222,9 +217,14 @@ public class BotController(
/// </summary>
/// <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 List<BotBase> GenerateBotWave(GenerateCondition generateRequest, BotGenerationDetails botGenerationDetails, string sessionId)
protected void GenerateBotWave(
GenerateCondition generateRequest,
BotGenerationDetails botGenerationDetails,
List<BotBase> botList,
string sessionId)
{
var isEventBot = generateRequest.Role?.Contains("event", StringComparison.OrdinalIgnoreCase);
if (isEventBot.GetValueOrDefault(false))
@@ -243,30 +243,36 @@ public class BotController(
_logger.Debug($"Generating wave of: {botGenerationDetails.BotCountToGenerate} bots of type: {role} {botGenerationDetails.BotDifficulty}");
}
var results = new List<BotBase>();
for (var i = 0; i < botGenerationDetails.BotCountToGenerate; i++)
Parallel.For(0, botGenerationDetails.BotCountToGenerate.Value, (i) =>
{
BotBase bot = null;
try
{
var bot = _botGenerator.PrepareAndGenerateBot(sessionId, _cloner.Clone(botGenerationDetails));
// 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 "Bear" or "Usec")
{
bot.Info.Side = "Savage";
}
results.Add(bot);
// Store bot details in cache so post-raid PMC messages can use data
_matchBotDetailsCacheService.CacheBot(bot);
bot = _botGenerator.PrepareAndGenerateBot(sessionId, _cloner.Clone(botGenerationDetails));
}
catch (Exception e)
{
_logger.Error($"Failed to generate bot: {botGenerationDetails.Role} #{i + 1}: {e.Message} {e.StackTrace}");
_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);
});
if (_logger.IsLogEnabled(LogLevel.Debug))
{
@@ -275,8 +281,6 @@ public class BotController(
$"({botGenerationDetails.EventRole ?? botGenerationDetails.Role ?? ""}) {botGenerationDetails.BotDifficulty} bots"
);
}
return results;
}
/// <summary>
@@ -314,17 +318,15 @@ public class BotController(
/// <param name="pmcProfile">Player who is generating bots</param>
/// <param name="allPmcsHaveSameNameAsPlayer">Should all PMCs have same name as player</param>
/// <param name="raidSettings">Settings chosen pre-raid by player in client</param>
/// <param name="botCountToGenerate">How many bots to generate</param>
/// <param name="generateAsPmc">Force bot being generated to be a PMC</param>
/// <returns>BotGenerationDetails</returns>
protected BotGenerationDetails GetBotGenerationDetailsForWave(
GenerateCondition condition,
PmcData? pmcProfile,
bool allPmcsHaveSameNameAsPlayer,
GetRaidConfigurationRequestData? raidSettings,
int? botCountToGenerate,
bool generateAsPmc)
GetRaidConfigurationRequestData? raidSettings)
{
var generateAsPmc = _botHelper.IsBotPmc(condition.Role);
return new BotGenerationDetails
{
IsPmc = generateAsPmc,
@@ -334,7 +336,7 @@ public class BotController(
PlayerName = pmcProfile?.Info?.Nickname,
BotRelativeLevelDeltaMax = _pmcConfig.BotRelativeLevelDeltaMax,
BotRelativeLevelDeltaMin = _pmcConfig.BotRelativeLevelDeltaMin,
BotCountToGenerate = botCountToGenerate,
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
IsPlayerScav = false,
@@ -1,4 +1,5 @@
using SPTarkov.Common.Annotations;
using SPTarkov.Server.Core.Constants;
using SPTarkov.Server.Core.Helpers;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Eft.Common;
@@ -12,6 +13,7 @@ using SPTarkov.Server.Core.Services;
using SPTarkov.Server.Core.Utils;
using SPTarkov.Server.Core.Utils.Cloners;
using BodyPart = SPTarkov.Server.Core.Models.Eft.Common.Tables.BodyPart;
using BodyParts = SPTarkov.Server.Core.Constants.BodyParts;
using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
@@ -53,12 +55,12 @@ public class BotGenerator(
var bot = GetCloneOfBotBase();
bot.Info.Settings.BotDifficulty = difficulty;
bot.Info.Settings.Role = role;
bot.Info.Side = "Savage";
bot.Info.Side = Sides.Savage;
var botGenDetails = new BotGenerationDetails
{
IsPmc = false,
Side = "Savage",
Side = Sides.Savage,
Role = role,
BotRelativeLevelDeltaMax = 0,
BotRelativeLevelDeltaMin = 0,
@@ -116,7 +118,7 @@ public class BotGenerator(
public BotBase PrepareAndGenerateBot(string sessionId, BotGenerationDetails? botGenerationDetails)
{
var preparedBotBase = GetPreparedBotBase(
botGenerationDetails.EventRole ?? botGenerationDetails.Role, // Use eventRole if provided,
botGenerationDetails.EventRole ?? botGenerationDetails.Role, // Use eventRole if provided
botGenerationDetails.Side,
botGenerationDetails.BotDifficulty
);
@@ -306,7 +308,7 @@ public class BotGenerator(
/// <returns>True if name should be simulated pscav</returns>
public bool ShouldSimulatePlayerScav(string botRole)
{
return botRole == "assault" && _randomUtil.GetChance100(_botConfig.ChanceAssaultScavHasPlayerScavName);
return botRole == Roles.Assault && _randomUtil.GetChance100(_botConfig.ChanceAssaultScavHasPlayerScavName);
}
/// <summary>
@@ -504,7 +506,7 @@ public class BotGenerator(
BodyParts = new Dictionary<string, BodyPartHealth>
{
{
"Head", new BodyPartHealth
BodyParts.Head, new BodyPartHealth
{
Health = new CurrentMinMax
{
@@ -514,7 +516,7 @@ public class BotGenerator(
}
},
{
"Chest", new BodyPartHealth
BodyParts.Chest, new BodyPartHealth
{
Health = new CurrentMinMax
{
@@ -524,7 +526,7 @@ public class BotGenerator(
}
},
{
"Stomach", new BodyPartHealth
BodyParts.Stomach, new BodyPartHealth
{
Health = new CurrentMinMax
{
@@ -534,7 +536,7 @@ public class BotGenerator(
}
},
{
"LeftArm", new BodyPartHealth
BodyParts.LeftArm, new BodyPartHealth
{
Health = new CurrentMinMax
{
@@ -544,7 +546,7 @@ public class BotGenerator(
}
},
{
"RightArm", new BodyPartHealth
BodyParts.RightArm, new BodyPartHealth
{
Health = new CurrentMinMax
{
@@ -554,7 +556,7 @@ public class BotGenerator(
}
},
{
"LeftLeg", new BodyPartHealth
BodyParts.LeftLeg, new BodyPartHealth
{
Health = new CurrentMinMax
{
@@ -564,7 +566,7 @@ public class BotGenerator(
}
},
{
"RightLeg", new BodyPartHealth
BodyParts.RightLeg, new BodyPartHealth
{
Health = new CurrentMinMax
{
@@ -783,7 +785,7 @@ public class BotGenerator(
Id = _hashUtil.Generate(),
Template = GetDogtagTplByGameVersionAndSide(bot.Info.Side, bot.Info.GameVersion),
ParentId = bot.Inventory.Equipment,
SlotId = "Dogtag",
SlotId = Slots.Dogtag,
Upd = new Upd
{
SpawnedInSession = true
@@ -801,7 +803,7 @@ public class BotGenerator(
/// <returns>item tpl</returns>
public string GetDogtagTplByGameVersionAndSide(string side, string gameVersion)
{
if (string.Equals(side, "usec", StringComparison.OrdinalIgnoreCase))
if (string.Equals(side, Sides.Usec, StringComparison.OrdinalIgnoreCase))
{
switch (gameVersion)
{
@@ -1,5 +1,6 @@
using System.Collections.Frozen;
using SPTarkov.Common.Annotations;
using SPTarkov.Server.Core.Constants;
using SPTarkov.Server.Core.Context;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Eft.Match;
@@ -15,22 +16,50 @@ using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
namespace SPTarkov.Server.Core.Helpers;
[Injectable]
public class BotGeneratorHelper(
ISptLogger<BotGeneratorHelper> _logger,
RandomUtil _randomUtil,
DurabilityLimitsHelper _durabilityLimitsHelper,
ItemHelper _itemHelper,
InventoryHelper _inventoryHelper,
ContainerHelper _containerHelper,
ApplicationContext _applicationContext,
LocalisationService _localisationService,
ConfigServer _configServer
)
public class BotGeneratorHelper
{
// Equipment slot ids that do not conflict with other slots
protected static readonly FrozenSet<string> _slotsWithNoCompatIssues = ["Scabbard", "Backpack", "SecureContainer", "Holster", "ArmBand"];
protected BotConfig _botConfig = _configServer.GetConfig<BotConfig>();
protected PmcConfig _pmcConfig = _configServer.GetConfig<PmcConfig>();
private static readonly FrozenSet<string> _slotsWithNoCompatIssues = [
EquipmentSlots.Scabbard.ToString(),
EquipmentSlots.Backpack.ToString(),
EquipmentSlots.SecuredContainer.ToString(),
EquipmentSlots.Holster.ToString(),
EquipmentSlots.ArmBand.ToString()
];
private readonly BotConfig _botConfig;
private readonly ISptLogger<BotGeneratorHelper> _logger;
private readonly RandomUtil _randomUtil;
private readonly DurabilityLimitsHelper _durabilityLimitsHelper;
private readonly ItemHelper _itemHelper;
private readonly InventoryHelper _inventoryHelper;
private readonly ContainerHelper _containerHelper;
private readonly ApplicationContext _applicationContext;
private readonly LocalisationService _localisationService;
private readonly string[] _pmcTypes;
public BotGeneratorHelper(ISptLogger<BotGeneratorHelper> logger,
RandomUtil randomUtil,
DurabilityLimitsHelper durabilityLimitsHelper,
ItemHelper itemHelper,
InventoryHelper inventoryHelper,
ContainerHelper containerHelper,
ApplicationContext applicationContext,
LocalisationService localisationService,
ConfigServer configServer)
{
_logger = logger;
_randomUtil = randomUtil;
_durabilityLimitsHelper = durabilityLimitsHelper;
_itemHelper = itemHelper;
_inventoryHelper = inventoryHelper;
_containerHelper = containerHelper;
_applicationContext = applicationContext;
_localisationService = localisationService;
_botConfig = configServer.GetConfig<BotConfig>();
var pmcConfig = configServer.GetConfig<PmcConfig>();
_pmcTypes = [pmcConfig.UsecType.ToLower(), pmcConfig.BearType.ToLower()];
}
/// <summary>
/// Adds properties to an item
@@ -392,9 +421,8 @@ public class BotGeneratorHelper(
}
// Does an equipped item have a property that blocks the desired item - check for prop "BlocksX" .e.g BlocksEarpiece / BlocksFaceCover
var blockingPropertyName = $"blocks{equipmentSlot}";
var templateItems = equippedItemsDb.ToList();
var blockingItem = templateItems.FirstOrDefault(item => HasBlockingProperty(item, blockingPropertyName));
var blockingItem = templateItems.FirstOrDefault(item => HasBlockingProperty(item, equipmentSlot));
if (blockingItem is not null)
// this.logger.warning(`1 incompatibility found between - {itemToEquip[1]._name} and {blockingItem._name} - {equipmentSlot}`);
{
@@ -424,7 +452,7 @@ public class BotGeneratorHelper(
// Does item being checked get blocked/block existing item
if (itemToEquip.Properties.BlocksHeadwear ?? false)
{
var existingHeadwear = itemsEquipped.FirstOrDefault(x => x.SlotId == "Headwear");
var existingHeadwear = itemsEquipped.FirstOrDefault(x => x.SlotId == Containers.Headwear);
if (existingHeadwear is not null)
{
return new ChooseRandomCompatibleModResult
@@ -440,7 +468,7 @@ public class BotGeneratorHelper(
// Does item being checked get blocked/block existing item
if (itemToEquip.Properties.BlocksFaceCover.GetValueOrDefault(false))
{
var existingFaceCover = itemsEquipped.FirstOrDefault(item => item.SlotId == "FaceCover");
var existingFaceCover = itemsEquipped.FirstOrDefault(item => item.SlotId == Containers.FaceCover);
if (existingFaceCover is not null)
{
return new ChooseRandomCompatibleModResult
@@ -456,7 +484,7 @@ public class BotGeneratorHelper(
// Does item being checked get blocked/block existing item
if (itemToEquip.Properties.BlocksEarpiece.GetValueOrDefault(false))
{
var existingEarpiece = itemsEquipped.FirstOrDefault(item => item.SlotId == "Earpiece");
var existingEarpiece = itemsEquipped.FirstOrDefault(item => item.SlotId == Containers.Earpiece);
if (existingEarpiece is not null)
{
return new ChooseRandomCompatibleModResult
@@ -472,7 +500,7 @@ public class BotGeneratorHelper(
// Does item being checked get blocked/block existing item
if (itemToEquip.Properties.BlocksArmorVest.GetValueOrDefault(false))
{
var existingArmorVest = itemsEquipped.FirstOrDefault(item => item.SlotId == "ArmorVest");
var existingArmorVest = itemsEquipped.FirstOrDefault(item => item.SlotId == Containers.ArmorVest);
if (existingArmorVest is not null)
{
return new ChooseRandomCompatibleModResult
@@ -507,10 +535,7 @@ public class BotGeneratorHelper(
protected bool HasBlockingProperty(TemplateItem? item, string blockingPropertyName)
{
return item?.Properties?.GetType().GetProperties()
.FirstOrDefault(x => x.PropertyType == typeof(bool)
&& x.Name.ToLower() == blockingPropertyName
&& (bool) x.GetValue(item.Properties)) is not null;
return item != null && item.Blocks.TryGetValue(blockingPropertyName, out var blocks) && blocks;
}
/// <summary>
@@ -520,11 +545,8 @@ public class BotGeneratorHelper(
/// <returns>Equipment role (e.g. pmc / assault / bossTagilla)</returns>
public string GetBotEquipmentRole(string botRole)
{
string[] pmcs = [_pmcConfig.UsecType.ToLower(), _pmcConfig.BearType.ToLower()];
return pmcs.Contains(
botRole.ToLower()
)
? "pmc"
return _pmcTypes.Contains(botRole, StringComparer.OrdinalIgnoreCase)
? Sides.PmcEquipmentRole
: botRole;
}
@@ -532,7 +554,7 @@ public class BotGeneratorHelper(
/// Adds an item with all its children into specified equipmentSlots, wherever it fits.
/// </summary>
/// <param name="equipmentSlots">Slot to add item+children into</param>
/// <param name="rootItemId">Root item id to use as mod items parentid</param>
/// <param name="rootItemId">Root item id to use as mod items parentId</param>
/// <param name="rootItemTplId">Root itms tpl id</param>
/// <param name="itemWithChildren">Item to add</param>
/// <param name="inventory">Inventory to add item+children into</param>
@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Collections.Frozen;
using SPTarkov.Common.Annotations;
using SPTarkov.Server.Core.Constants;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Models.Utils;
@@ -18,10 +19,17 @@ public class BotHelper(
ConfigServer _configServer
)
{
protected static readonly FrozenSet<string> _pmcTypeIds = ["usec", "bear", "pmc", "pmcbear", "pmcusec"];
protected BotConfig _botConfig = _configServer.GetConfig<BotConfig>();
protected PmcConfig _pmcConfig = _configServer.GetConfig<PmcConfig>();
protected ConcurrentDictionary<string, List<string>> _pmcNameCache = new();
private static readonly FrozenSet<string> _pmcTypeIds =
[
Sides.Usec.ToLower(),
Sides.Bear.ToLower(),
Sides.PmcBear.ToLower(),
Sides.PmcUsec.ToLower()
];
private readonly BotConfig _botConfig = _configServer.GetConfig<BotConfig>();
private readonly PmcConfig _pmcConfig = _configServer.GetConfig<PmcConfig>();
private readonly ConcurrentDictionary<string, List<string>> _pmcNameCache = new();
/// <summary>
/// Get a template object for the specified botRole from bots.types db
@@ -163,12 +171,12 @@ public class BotHelper(
{
if (string.Equals(_pmcConfig.BearType, botRole, StringComparison.OrdinalIgnoreCase))
{
return "Bear";
return Sides.Bear;
}
if (string.Equals(_pmcConfig.UsecType, botRole.ToLower(), StringComparison.OrdinalIgnoreCase))
if (string.Equals(_pmcConfig.UsecType, botRole, StringComparison.OrdinalIgnoreCase))
{
return "Usec";
return Sides.Usec;
}
return GetRandomizedPmcSide();
@@ -180,7 +188,7 @@ public class BotHelper(
/// <returns>pmc side as string</returns>
protected string GetRandomizedPmcSide()
{
return _randomUtil.GetChance100(_pmcConfig.IsUsec) ? "Usec" : "Bear";
return _randomUtil.GetChance100(_pmcConfig.IsUsec) ? Sides.Usec : Sides.Bear;
}
/// <summary>
@@ -191,14 +199,14 @@ public class BotHelper(
/// <returns>name of PMC</returns>
public string GetPmcNicknameOfMaxLength(int maxLength, string? side = null)
{
var chosenFaction = (side ?? (_randomUtil.GetInt(0, 1) == 0 ? "usec" : "bear")).ToLowerInvariant();
var chosenFaction = (side ?? (_randomUtil.GetInt(0, 1) == 0 ? Sides.Usec : Sides.Bear)).ToLowerInvariant();
var cacheKey = $"{chosenFaction}{maxLength}";
if (!_pmcNameCache.TryGetValue(cacheKey, out var eligibleNames))
{
if (!_databaseService.GetBots().Types.TryGetValue(chosenFaction, out var chosenFactionDetails))
{
_logger.Error($"Unknown faction: {chosenFaction} Defaulting to: USEC");
chosenFaction = "usec";
_logger.Error($"Unknown faction: {chosenFaction} Defaulting to: {Sides.Usec}");
chosenFaction = Sides.Usec.ToLower();
chosenFactionDetails = _databaseService.GetBots().Types[chosenFaction];
}
@@ -206,7 +214,7 @@ public class BotHelper(
if (!matchingNames.Any())
{
_logger.Warning(
$"Unable to filter: {chosenFaction} PMC names to only those under: {maxLength}, none found that match that criteria, selecting from entire name pool instead`,\n"
$"Unable to filter: {chosenFaction} PMC names to only those under: {maxLength}, none found that match that criteria, selecting from entire name pool instead"
);
// Return a random string from names
@@ -110,7 +110,7 @@ public class DurabilityLimitsHelper(
var roleExistsInConfig = _botConfig.Durability.BotDurabilities.ContainsKey(botRole);
if (!roleExistsInConfig)
{
_logger.Warning($"{botRole} doesn't exist in bot config durability values, using default fallback");
_logger.Debug($"{botRole} doesn't exist in bot config durability values, using default fallback");
return "default";
}
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using SPTarkov.Server.Core.Constants;
using SPTarkov.Server.Core.Models.Enums;
using SPTarkov.Server.Core.Utils.Json.Converters;
@@ -6,6 +7,8 @@ namespace SPTarkov.Server.Core.Models.Eft.Common.Tables;
public record TemplateItem
{
private Dictionary<string, bool>? _blocks;
private string? _id;
private string? _name;
@@ -87,6 +90,27 @@ public record TemplateItem
_prototype = string.Intern(value);
}
}
/// <summary>
/// Used for easy access during bot generation to any slot/container this item is blocking.
/// </summary>
[JsonIgnore]
public Dictionary<string, bool> Blocks
{
get
{
return _blocks ??= new Dictionary<string, bool>()
{
{ Containers.LeftStance, Properties?.BlockLeftStance ?? false },
{ Containers.Collapsible, Properties?.BlocksCollapsible ?? false },
{ Containers.Earpiece, Properties?.BlocksEarpiece ?? false },
{ Containers.Eyewear, Properties?.BlocksEyewear ?? false },
{ Containers.FaceCover, Properties?.BlocksFaceCover ?? false },
{ Containers.Folding, Properties?.BlocksFolding ?? false },
{ Containers.Headwear, Properties?.BlocksHeadwear ?? false }
};
}
}
}
public record Props
@@ -8,6 +8,7 @@ using SPTarkov.Server.Core.Models.Enums;
using SPTarkov.Server.Core.Models.Spt.Bots;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Utils.Cloners;
using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
namespace SPTarkov.Server.Core.Services;
@@ -17,7 +18,6 @@ public class BotLootCacheService(
ItemHelper _itemHelper,
PMCLootGenerator _pmcLootGenerator,
LocalisationService _localisationService,
RagfairPriceService _ragfairPriceService,
ICloner _cloner
)
{
@@ -580,7 +580,10 @@ public class BotLootCacheService(
)
)
{
_logger.Info($"Unable to add loot cache for bot role: {botRole} - already exists");
if (_logger.IsLogEnabled(LogLevel.Debug))
{
_logger.Debug($"Unable to add loot cache for bot role: {botRole} - already exists");
}
}
}
@@ -84,9 +84,14 @@ public class LocalisationService
/// <returns> Locale text </returns>
public string GetRandomTextThatMatchesPartialKey(string partialKey)
{
var values = _localeService.GetLocaleKeysThatStartsWithValue(partialKey);
var chosenKey = _randomUtil.GetArrayValue(values);
var matchingKeys = GetKeys().Where(x => x.Contains(partialKey)).ToList();
if (!matchingKeys.Any())
{
_logger.Warning($"No locale keys found for: {partialKey}");
return GetText(chosenKey);
return string.Empty;
}
return GetText(_randomUtil.GetArrayValue(matchingKeys));
}
}
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using SPTarkov.Common.Annotations;
using SPTarkov.Server.Core.Constants;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Enums;
using SPTarkov.Server.Core.Models.Spt.Bots;
@@ -8,7 +9,7 @@ using SPTarkov.Server.Core.Models.Utils;
namespace SPTarkov.Server.Core.Services;
/// <summary>
/// Cache bots in a dictionary, keyed by the bots name, keying by name isnt ideal as its not unique but this is used by the post-raid system which doesnt have any bot ids, only name
/// Cache bots in a dictionary, keyed by the bots ID
/// </summary>
[Injectable(InjectionType.Singleton)]
public class MatchBotDetailsCacheService(
@@ -17,14 +18,14 @@ public class MatchBotDetailsCacheService(
{
private static readonly HashSet<string> _sidesToCache =
[
"pmcUSEC",
"pmcBEAR"
Sides.PmcUsec,
Sides.PmcBear
];
protected readonly ConcurrentDictionary<string, BotDetailsForChatMessages> BotDetailsCache = new();
/// <summary>
/// Store a bot in the cache, keyed by its name.
/// Store a bot in the cache, keyed by its ID.
/// </summary>
/// <param name="botToCache"> Bot details to cache </param>
public void CacheBot(BotBase botToCache)
@@ -49,7 +50,7 @@ public class MatchBotDetailsCacheService(
BotDetailsCache.TryAdd(botToCache.Id, new BotDetailsForChatMessages()
{
Nickname = botToCache.Info.Nickname.Trim(),
Side = botToCache.Info.Side == "pmcUSEC" ? DogtagSide.Usec : DogtagSide.Bear,
Side = botToCache.Info.Side == Sides.PmcUsec ? DogtagSide.Usec : DogtagSide.Bear,
Aid = botToCache.Aid,
Type = botToCache.Info.MemberCategory,
Level = botToCache.Info.Level,
@@ -65,10 +66,9 @@ public class MatchBotDetailsCacheService(
}
/// <summary>
/// Find a bot in the cache by its name and side.
/// Find a bot in the cache by its ID.
/// </summary>
/// <param name="botName"> Name of bot to find </param>
/// <param name="botSide"> Side of the bot </param>
/// <param name="id"> ID of bot to find </param>
/// <returns></returns>
public BotDetailsForChatMessages? GetBotById(string? id)
{