Files
SPT-Server-Build/Core/Generators/BotInventoryGenerator.cs
T
2025-01-18 19:54:33 +00:00

640 lines
26 KiB
C#

using Core.Annotations;
using Core.Context;
using Core.Helpers;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Match;
using Core.Models.Enums;
using Core.Models.Spt.Bots;
using Core.Models.Spt.Config;
using Core.Models.Utils;
using Core.Servers;
using Core.Services;
using Core.Utils;
using Equipment = Core.Models.Eft.Common.Tables.Equipment;
namespace Core.Generators;
[Injectable]
public class BotInventoryGenerator(
ISptLogger<BotInventoryGenerator> _logger,
HashUtil _hashUtil,
RandomUtil _randomUtil,
DatabaseService _databaseService,
ApplicationContext _applicationContext,
BotWeaponGenerator _botWeaponGenerator,
BotLootGenerator _botLootGenerator,
BotGeneratorHelper _botGeneratorHelper,
ProfileHelper _profileHelper,
BotHelper _botHelper,
WeightedRandomHelper _weightedRandomHelper,
ItemHelper _itemHelper,
WeatherHelper _weatherHelper,
LocalisationService _localisationService,
BotEquipmentFilterService _botEquipmentFilterService,
BotEquipmentModPoolService _botEquipmentModPoolService,
BotEquipmentModGenerator _botEquipmentModGenerator,
ConfigServer _configServer
)
{
private BotConfig _botConfig = _configServer.GetConfig<BotConfig>();
/// <summary>
/// Add equipment/weapons/loot to bot
/// </summary>
/// <param name="sessionId">Session id</param>
/// <param name="botJsonTemplate">Base json db file for the bot having its loot generated</param>
/// <param name="botRole">Role bot has (assault/pmcBot)</param>
/// <param name="isPmc">Is bot being converted into a pmc</param>
/// <param name="botLevel">Level of bot being generated</param>
/// <param name="chosenGameVersion">Game version for bot, only really applies for PMCs</param>
/// <returns>PmcInventory object with equipment/weapons/loot</returns>
public BotBaseInventory GenerateInventory(string sessionId, BotType botJsonTemplate, string botRole, bool isPmc, int botLevel, string chosenGameVersion)
{
var templateInventory = botJsonTemplate.BotInventory;
var wornItemChances = botJsonTemplate.BotChances;
var itemGenerationLimitsMinMax = botJsonTemplate.BotGeneration;
// Generate base inventory with no items
var botInventory = GenerateInventoryBase();
// Get generated raid details bot will be spawned in
var raidConfig = _applicationContext
.GetLatestValue(ContextVariableType.RAID_CONFIGURATION)
?.GetValue<GetRaidConfigurationRequestData>();
GenerateAndAddEquipmentToBot(
sessionId,
templateInventory,
wornItemChances,
botRole,
botInventory,
botLevel,
chosenGameVersion,
raidConfig
);
// Roll weapon spawns (primary/secondary/holster) and generate a weapon for each roll that passed
GenerateAndAddWeaponsToBot(
templateInventory,
wornItemChances,
sessionId,
botInventory,
botRole,
isPmc,
itemGenerationLimitsMinMax,
botLevel
);
// Pick loot and add to bots containers (rig/backpack/pockets/secure)
_botLootGenerator.GenerateLoot(sessionId, botJsonTemplate, isPmc, botRole, botInventory, botLevel);
return botInventory;
}
/// <summary>
/// Create a pmcInventory object with all the base/generic items needed
/// </summary>
/// <returns>PmcInventory object</returns>
public BotBaseInventory GenerateInventoryBase()
{
var equipmentId = _hashUtil.Generate();
var stashId = _hashUtil.Generate();
var questRaidItemsId = _hashUtil.Generate();
var questStashItemsId = _hashUtil.Generate();
var sortingTableId = _hashUtil.Generate();
return new BotBaseInventory
{
Items =
[
new() { Id = equipmentId, Template = ItemTpl.INVENTORY_DEFAULT },
new() { Id = stashId, Template = ItemTpl.STASH_STANDARD_STASH_10X30 },
new() { Id = questRaidItemsId, Template = ItemTpl.STASH_QUESTRAID },
new() { Id = questStashItemsId, Template = ItemTpl.STASH_QUESTOFFLINE },
new() { Id = sortingTableId, Template = ItemTpl.SORTINGTABLE_SORTING_TABLE }
],
Equipment = equipmentId,
Stash = stashId,
QuestRaidItems = questRaidItemsId,
QuestStashItems = questStashItemsId,
SortingTable = sortingTableId,
HideoutAreaStashes = { },
FastPanel = { },
FavoriteItems = [],
HideoutCustomizationStashId = "",
};
}
/// <summary>
/// Add equipment to a bot
/// </summary>
/// <param name="sessionId">Session id</param>
/// <param name="templateInventory">bot/x.json data from db</param>
/// <param name="wornItemChances">Chances items will be added to bot</param>
/// <param name="botRole">Role bot has (assault/pmcBot)</param>
/// <param name="botInventory">Inventory to add equipment to</param>
/// <param name="botLevel">Level of bot</param>
/// <param name="chosenGameVersion">Game version for bot, only really applies for PMCs</param>
/// <param name="raidConfig">RadiConfig</param>
public void GenerateAndAddEquipmentToBot(string sessionId, BotTypeInventory templateInventory, Chances wornItemChances, string botRole,
BotBaseInventory botInventory, int botLevel, string chosenGameVersion, GetRaidConfigurationRequestData raidConfig)
{
// These will be handled later
var excludedSlots = new List<EquipmentSlots>()
{
EquipmentSlots.Pockets,
EquipmentSlots.FirstPrimaryWeapon,
EquipmentSlots.SecondPrimaryWeapon,
EquipmentSlots.Holster,
EquipmentSlots.ArmorVest,
EquipmentSlots.TacticalVest,
EquipmentSlots.FaceCover,
EquipmentSlots.Headwear,
EquipmentSlots.Earpiece
};
_botConfig.Equipment.TryGetValue(_botGeneratorHelper.GetBotEquipmentRole(botRole), out var botEquipConfig);
var randomistionDetails = _botHelper.GetBotRandomizationDetails(botLevel, botEquipConfig);
// Apply nighttime changes if its nighttime + there's changes to make
if (
randomistionDetails?.NighttimeChanges is not null &&
raidConfig is not null &&
_weatherHelper.IsNightTime(raidConfig.TimeVariant)
)
{
foreach (var equipmentSlotKvP in (randomistionDetails.NighttimeChanges.EquipmentModsModifiers))
{
// Never let mod chance go outside of 0 - 100
randomistionDetails.EquipmentMods[equipmentSlotKvP.Key] = Math.Min(
Math.Max(randomistionDetails.EquipmentMods[equipmentSlotKvP.Key] + equipmentSlotKvP.Value, 0),
100
);
}
}
// Get profile of player generating bots, we use their level later on
var pmcProfile = _profileHelper.GetPmcProfile(sessionId);
var botEquipmentRole = _botGeneratorHelper.GetBotEquipmentRole(botRole);
// Iterate over all equipment slots of bot, do it in specifc order to reduce conflicts
// e.g. ArmorVest should be generated after TactivalVest
// or FACE_COVER before HEADWEAR
foreach (var (equipmentSlot, value) in templateInventory.Equipment)
{
// Skip some slots as they need to be done in a specific order + with specific parameter values
// e.g. Weapons
if (excludedSlots.Contains(equipmentSlot))
{
continue;
}
GenerateEquipment(
new GenerateEquipmentProperties
{
RootEquipmentSlot = equipmentSlot,
RootEquipmentPool = value,
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
}
);
}
// Generate below in specific order
GenerateEquipment(
new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.Pockets,
// Unheard profiles have unique sized pockets, TODO - handle this somewhere else in a better way
RootEquipmentPool =
chosenGameVersion == GameEditions.UNHEARD
? new Dictionary<string, double> { [ItemTpl.POCKETS_1X4_TUE] = 1 }
: templateInventory.Equipment[EquipmentSlots.Pockets],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GenerateModsBlacklist = [ItemTpl.POCKETS_1X4_TUE],
GeneratingPlayerLevel = pmcProfile.Info.Level,
}
);
GenerateEquipment(
new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.FaceCover,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.FaceCover],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
}
);
GenerateEquipment(
new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.Headwear,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Headwear],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
}
);
GenerateEquipment(
new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.Earpiece,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Earpiece],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
}
);
var hasArmorVest = GenerateEquipment(
new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.ArmorVest,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.ArmorVest],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
}
);
// Bot has no armor vest and flagged to be forced to wear armored rig in this event
if (botEquipConfig.ForceOnlyArmoredRigWhenNoArmor.GetValueOrDefault(false) && !hasArmorVest)
{
// Filter rigs down to only those with armor
FilterRigsToThoseWithProtection(templateInventory.Equipment, botRole);
}
// Optimisation - Remove armored rigs from pool
if (hasArmorVest)
{
// Filter rigs down to only those with armor
FilterRigsToThoseWithoutProtection(templateInventory.Equipment, botRole);
}
// Bot is flagged as always needing a vest
if (botEquipConfig.ForceRigWhenNoVest.GetValueOrDefault(false) && !hasArmorVest)
{
wornItemChances.EquipmentChances["TacticalVest"] = 100;
}
GenerateEquipment(
new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.Earpiece,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Earpiece],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
}
);
}
/// <summary>
/// Remove non-armored rigs from parameter data
/// </summary>
/// <param name="templateEquipment">Equpiment to filter TacticalVest of</param>
/// <param name="botRole">Role of bot vests are being filtered for</param>
public void FilterRigsToThoseWithProtection(Dictionary<EquipmentSlots, Dictionary<string, double>> templateEquipment, string botRole)
{
var tacVestsWithArmor = templateEquipment[EquipmentSlots.TacticalVest]
.Where(kvp => _itemHelper.ItemHasSlots(kvp.Key))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (!tacVestsWithArmor.Any())
{
_logger.Debug($"Unable to filter to only armored rigs as bot: {botRole} has none in pool");
return;
}
templateEquipment[EquipmentSlots.TacticalVest] = tacVestsWithArmor;
}
/// <summary>
/// Remove armored rigs from parameter data
/// </summary>
/// <param name="templateEquipment">Equipment to filter TacticalVest by</param>
/// <param name="botRole">Role of bot vests are being filtered for</param>
/// <param name="allowEmptyResult">Should the function return all rigs when 0 unarmored are found</param>
public void FilterRigsToThoseWithoutProtection(Dictionary<EquipmentSlots, Dictionary<string, double>> templateEquipment, string botRole,
bool allowEmptyResult = true)
{
var tacVestsWithoutArmor = templateEquipment[EquipmentSlots.TacticalVest]
.Where(kvp => !_itemHelper.ItemHasSlots(kvp.Key))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (!allowEmptyResult && !tacVestsWithoutArmor.Any())
{
_logger.Debug($"Unable to filter to only unarmored rigs as bot: {botRole} has none in pool");
return;
}
templateEquipment[EquipmentSlots.TacticalVest] = tacVestsWithoutArmor;
}
/// <summary>
/// Add a piece of equipment with mods to inventory from the provided pools
/// </summary>
/// <param name="settings">Values to adjust how item is chosen and added to bot</param>
/// <returns>true when item added</returns>
public bool GenerateEquipment(GenerateEquipmentProperties settings)
{
List<string> slotsToCheck = [EquipmentSlots.Pockets.ToString(), EquipmentSlots.SecuredContainer.ToString()];
double? spawnChance = slotsToCheck.Contains(settings.RootEquipmentSlot.ToString())
? 100
: settings.SpawnChances.EquipmentChances.GetValueOrDefault(settings.RootEquipmentSlot.ToString());
if (!spawnChance.HasValue)
{
_logger.Warning(
_localisationService.GetText(
"bot-no_spawn_chance_defined_for_equipment_slot",
settings.RootEquipmentSlot
)
);
return false;
}
// Roll dice on equipment item
var shouldSpawn = _randomUtil.GetChance100(spawnChance ?? 0);
if (shouldSpawn && !settings.RootEquipmentPool.Any())
{
var pickedItemDb = new TemplateItem();
var found = false;
// Limit attempts to find a compatible item as it's expensive to check them all
var maxAttempts = Math.Round(settings.RootEquipmentPool.Count() * 0.75); // Roughly 75% of pool size
var attempts = 0;
while (!found)
{
if (!settings.RootEquipmentPool.Any())
{
return false;
}
var chosenItemTpl = _weightedRandomHelper.GetWeightedValue(settings.RootEquipmentPool);
var dbResult = _itemHelper.GetItem(chosenItemTpl);
if (!dbResult.Key)
{
_logger.Error(_localisationService.GetText("bot-missing_item_template", chosenItemTpl));
_logger.Debug($"EquipmentSlot-> {settings.RootEquipmentSlot}");
// Remove picked item
settings.RootEquipmentPool.Remove(chosenItemTpl);
attempts++;
continue;
}
// Is the chosen item compatible with other items equipped
var compatibilityResult = _botGeneratorHelper.IsItemIncompatibleWithCurrentItems(
settings.Inventory.Items,
chosenItemTpl,
settings.RootEquipmentSlot.ToString()
);
if (compatibilityResult.Incompatible ?? false)
{
// Tried x different items that failed, stop
if (attempts > maxAttempts)
{
return false;
}
// Remove picked item from pool
settings.RootEquipmentPool.Remove(chosenItemTpl);
// Increment times tried
attempts++;
}
else
{
// Success
found = true;
pickedItemDb = dbResult.Value;
}
}
// Create root item
var id = _hashUtil.Generate();
Item item = new()
{
Id = id,
Template = pickedItemDb.Id,
ParentId = settings.Inventory.Equipment,
SlotId = settings.RootEquipmentSlot.ToString(),
Upd = _botGeneratorHelper.GenerateExtraPropertiesForItem(pickedItemDb, settings.BotData.Role)
};
var botEquipBlacklist = _botEquipmentFilterService.GetBotEquipmentBlacklist(
settings.BotData.EquipmentRole,
(double)settings.GeneratingPlayerLevel
);
// Edge case: Filter the armor items mod pool if bot exists in config dict + config has armor slot
if (_botConfig.Equipment[settings.BotData.EquipmentRole] is not null &&
settings.RandomisationDetails.RandomisedArmorSlots.Contains(settings.RootEquipmentSlot.ToString()))
{
// Filter out mods from relevant blacklist
settings.ModPool[pickedItemDb.Id] = GetFilteredDynamicModsForItem(
pickedItemDb.Id,
botEquipBlacklist.Equipment
);
}
// Does item have slots for sub-mods to be inserted into
if (pickedItemDb.Properties.Slots.Any() && settings.GenerateModsBlacklist.Contains(pickedItemDb.Id))
{
var childItemsToAdd = _botEquipmentModGenerator.GenerateModsForEquipment(
[item],
id,
pickedItemDb,
settings,
botEquipBlacklist
);
settings.Inventory.Items.AddRange(childItemsToAdd);
}
else
{
// No slots, add root item only
settings.Inventory.Items.Add(item);
}
return true;
}
return false;
}
/// <summary>
/// Get all possible mods for item and filter down based on equipment blacklist from bot.json config
/// </summary>
/// <param name="itemTpl">Item mod pool is being retrieved and filtered</param>
/// <param name="equipmentBlacklist">Blacklist to filter mod pool with</param>
/// <returns>Filtered pool of mods</returns>
public Dictionary<string, List<string>> GetFilteredDynamicModsForItem(string itemTpl, Dictionary<string, List<string>> equipmentBlacklist)
{
var modPool = _botEquipmentModPoolService.GetModsForGearSlot(itemTpl);
foreach (var modSlot in modPool.Keys ?? Enumerable.Empty<string>())
{
var blacklistedMods = equipmentBlacklist[modSlot] ?? [];
var filteredMods = modPool[modSlot].Where((slotName) => !blacklistedMods.Contains(slotName));
if (filteredMods.Any())
{
modPool[modSlot] = filteredMods.ToList();
}
}
return modPool;
}
/// <summary>
/// Work out what weapons bot should have equipped and add them to bot inventory
/// </summary>
/// <param name="templateInventory">bot/x.json data from db</param>
/// <param name="equipmentChances">Chances bot can have equipment equipped</param>
/// <param name="sessionId">Session id</param>
/// <param name="botInventory">Inventory to add weapons to</param>
/// <param name="botRole">assault/pmcBot/bossTagilla etc</param>
/// <param name="isPmc">Is the bot being generated as a pmc</param>
/// <param name="itemGenerationLimitsMinMax">Limits for items the bot can have</param>
/// <param name="botLevel">level of bot having weapon generated</param>
public void GenerateAndAddWeaponsToBot(BotTypeInventory templateInventory, Chances equipmentChances, string sessionId, BotBaseInventory botInventory,
string botRole, bool isPmc, Generation itemGenerationLimitsMinMax, int botLevel)
{
var weaponSlotsToFill = GetDesiredWeaponsForBot(equipmentChances);
foreach (var desiredWeapons in weaponSlotsToFill)
{
// Add weapon to bot if true and bot json has something to put into the slot
if (desiredWeapons.ShouldSpawn && templateInventory.Equipment[desiredWeapons.Slot].Any())
{
AddWeaponAndMagazinesToInventory(
sessionId,
desiredWeapons,
templateInventory,
botInventory,
equipmentChances,
botRole,
isPmc,
itemGenerationLimitsMinMax,
botLevel
);
}
}
}
/// <summary>
/// Calculate if the bot should have weapons in Primary/Secondary/Holster slots
/// </summary>
/// <param name="equipmentChances">Chances bot has certain equipment</param>
/// <returns>What slots bot should have weapons generated for</returns>
public List<DesiredWeapons> GetDesiredWeaponsForBot(Chances equipmentChances)
{
var shouldSpawnPrimary = _randomUtil.GetChance100(equipmentChances.EquipmentChances["FirstPrimaryWeapon"]);
return
[
new()
{
Slot = EquipmentSlots.FirstPrimaryWeapon, ShouldSpawn = shouldSpawnPrimary
},
new()
{
Slot = EquipmentSlots.SecondPrimaryWeapon,
ShouldSpawn = shouldSpawnPrimary
? _randomUtil.GetChance100(equipmentChances.EquipmentChances["SecondPrimaryWeapon"])
: false
},
new()
{
Slot = EquipmentSlots.Holster,
ShouldSpawn = shouldSpawnPrimary
? _randomUtil.GetChance100(equipmentChances.EquipmentChances["Holster"]) // Primary weapon = roll for chance at pistol
: true // No primary = force pistol
}
];
}
/// <summary>
/// Add weapon + spare mags/ammo to bots inventory
/// </summary>
/// <param name="sessionId">Session id</param>
/// <param name="weaponSlot">Weapon slot being generated</param>
/// <param name="templateInventory">bot/x.json data from db</param>
/// <param name="botInventory">Inventory to add weapon+mags/ammo to</param>
/// <param name="equipmentChances">Chances bot can have equipment equipped</param>
/// <param name="botRole">assault/pmcBot/bossTagilla etc</param>
/// <param name="isPmc">Is the bot being generated as a pmc</param>
/// <param name="itemGenerationWeights"></param>
/// <param name="botLevel"></param>
public void AddWeaponAndMagazinesToInventory(string sessionId, DesiredWeapons weaponSlot, BotTypeInventory templateInventory, BotBaseInventory botInventory,
Chances equipmentChances, string botRole,
bool isPmc, Generation itemGenerationWeights, int botLevel)
{
var generatedWeapon = _botWeaponGenerator.GenerateRandomWeapon(
sessionId,
weaponSlot.Slot.ToString(),
templateInventory,
botInventory.Equipment,
equipmentChances.WeaponModsChances,
botRole,
isPmc,
botLevel
);
botInventory.Items.AddRange(generatedWeapon.Weapon);
_botWeaponGenerator.AddExtraMagazinesToInventory(
generatedWeapon,
itemGenerationWeights.Items.Magazines,
botInventory,
botRole
);
}
}
public class DesiredWeapons
{
public EquipmentSlots Slot { get; set; }
public bool ShouldSpawn { get; set; }
}