530 lines
22 KiB
C#
530 lines
22 KiB
C#
using System.Collections.Frozen;
|
|
using SPTarkov.DI.Annotations;
|
|
using SPTarkov.Server.Core.Constants;
|
|
using SPTarkov.Server.Core.Extensions;
|
|
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;
|
|
using SPTarkov.Server.Core.Models.Spt.Config;
|
|
using SPTarkov.Server.Core.Models.Utils;
|
|
using SPTarkov.Server.Core.Servers;
|
|
using SPTarkov.Server.Core.Services;
|
|
using SPTarkov.Server.Core.Utils;
|
|
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,
|
|
ProfileActivityService profileActivityService,
|
|
ServerLocalisationService serverLocalisationService,
|
|
BotInventoryContainerService botInventoryContainerService,
|
|
ConfigServer configServer
|
|
)
|
|
{
|
|
// Equipment slot ids that do not conflict with other slots
|
|
private static readonly FrozenSet<string> _slotsWithNoCompatIssues =
|
|
[
|
|
nameof(EquipmentSlots.Scabbard),
|
|
nameof(EquipmentSlots.Backpack),
|
|
nameof(EquipmentSlots.SecuredContainer),
|
|
nameof(EquipmentSlots.Holster),
|
|
nameof(EquipmentSlots.ArmBand),
|
|
];
|
|
|
|
private static readonly FrozenSet<string> _pmcTypes = [Sides.PmcBear.ToLowerInvariant(), Sides.PmcUsec.ToLowerInvariant()];
|
|
|
|
protected readonly BotConfig BotConfig = configServer.GetConfig<BotConfig>();
|
|
|
|
/// <summary>
|
|
/// Adds properties to an item
|
|
/// e.g. Repairable / HasHinge / Foldable / MaxDurability
|
|
/// </summary>
|
|
/// <param name="itemTemplate">Item extra properties are being generated for</param>
|
|
/// <param name="botRole">Used by weapons to randomize the durability values. Null for non-equipped items</param>
|
|
/// <param name="forceStackObjectsCount">Force property on item</param>
|
|
/// <returns>Item Upd object with extra properties</returns>
|
|
public Upd? GenerateExtraPropertiesForItem(TemplateItem? itemTemplate, string? botRole = null, bool forceStackObjectsCount = false)
|
|
{
|
|
// Get raid settings, if no raid, default to day
|
|
var raidSettings = profileActivityService.GetFirstProfileActivityRaidData()?.RaidConfiguration;
|
|
|
|
// BotRole property exists, we have specific bot randomisation values to make use of
|
|
RandomisedResourceDetails? randomisationSettings = null;
|
|
if (botRole is not null)
|
|
{
|
|
BotConfig.LootItemResourceRandomization.TryGetValue(botRole, out randomisationSettings);
|
|
}
|
|
|
|
Upd itemUpd = new();
|
|
var hasProperties = false;
|
|
|
|
if (itemTemplate?.Properties?.MaxDurability is not null && itemTemplate.Properties.MaxDurability > 0)
|
|
{
|
|
if (itemTemplate.Properties.WeapClass is not null)
|
|
{
|
|
// Is weapon
|
|
itemUpd.Repairable = GenerateWeaponRepairableProperties(itemTemplate, botRole);
|
|
hasProperties = true;
|
|
}
|
|
else if (itemTemplate.Properties.ArmorClass is not null)
|
|
{
|
|
// Is armor
|
|
itemUpd.Repairable = GenerateArmorRepairableProperties(itemTemplate, botRole);
|
|
hasProperties = true;
|
|
}
|
|
}
|
|
|
|
if (itemTemplate?.Properties?.HasHinge ?? false)
|
|
{
|
|
itemUpd.Togglable = new UpdTogglable { On = true };
|
|
hasProperties = true;
|
|
}
|
|
|
|
if (itemTemplate?.Properties?.Foldable ?? false)
|
|
{
|
|
itemUpd.Foldable = new UpdFoldable { Folded = false };
|
|
hasProperties = true;
|
|
}
|
|
|
|
if (itemTemplate?.Properties?.WeapFireType?.Count == 0)
|
|
{
|
|
itemUpd.FireMode = itemTemplate.Properties.WeapFireType.Contains("fullauto")
|
|
? new UpdFireMode { FireMode = "fullauto" }
|
|
: new UpdFireMode { FireMode = randomUtil.GetArrayValue(itemTemplate.Properties.WeapFireType) };
|
|
hasProperties = true;
|
|
}
|
|
|
|
// Must have value + not be 0 (e.g. Esmarch tourniquet) as they're single use
|
|
if (itemTemplate?.Properties?.MaxHpResource is not null && itemTemplate.Properties.MaxHpResource != 0)
|
|
{
|
|
itemUpd.MedKit = new UpdMedKit
|
|
{
|
|
HpResource = GetRandomizedResourceValue(itemTemplate.Properties.MaxHpResource ?? 0, randomisationSettings?.Meds),
|
|
};
|
|
hasProperties = true;
|
|
}
|
|
|
|
if (itemTemplate?.Properties?.MaxResource is not null && itemTemplate.Properties?.FoodUseTime is not null)
|
|
{
|
|
itemUpd.FoodDrink = new UpdFoodDrink
|
|
{
|
|
HpPercent = GetRandomizedResourceValue(itemTemplate.Properties.MaxResource ?? 0, randomisationSettings?.Food),
|
|
};
|
|
hasProperties = true;
|
|
}
|
|
|
|
var equipmentSettings = GetBotEquipmentSettingFromConfig(botRole);
|
|
if (itemTemplate?.Parent == BaseClasses.FLASHLIGHT)
|
|
{
|
|
var lightLaserActiveChance =
|
|
raidSettings?.IsNightRaid ?? false // Higher chance of laser/light at night
|
|
? equipmentSettings?.LightIsActiveNightChancePercent ?? 50
|
|
: equipmentSettings?.LightIsActiveDayChancePercent ?? 25;
|
|
|
|
itemUpd.Light = new UpdLight { IsActive = randomUtil.GetChance100(lightLaserActiveChance), SelectedMode = 0 };
|
|
hasProperties = true;
|
|
}
|
|
else if (itemTemplate?.Parent == BaseClasses.TACTICAL_COMBO)
|
|
{
|
|
// Get chance from botconfig for bot type, use 50% if no value found
|
|
var lightLaserActiveChance = equipmentSettings?.LaserIsActiveChancePercent ?? 50;
|
|
|
|
itemUpd.Light = new UpdLight { IsActive = randomUtil.GetChance100(lightLaserActiveChance), SelectedMode = 0 };
|
|
hasProperties = true;
|
|
}
|
|
|
|
if (itemTemplate?.Parent == BaseClasses.NIGHT_VISION)
|
|
{
|
|
// Get chance from botconfig for bot type
|
|
var nvgActiveChance =
|
|
raidSettings?.IsNightRaid ?? false
|
|
? equipmentSettings?.NvgIsActiveChanceNightPercent ?? 90
|
|
: equipmentSettings?.NvgIsActiveChanceDayPercent ?? 15;
|
|
|
|
itemUpd.Togglable = new UpdTogglable { On = randomUtil.GetChance100(nvgActiveChance) };
|
|
hasProperties = true;
|
|
}
|
|
|
|
// Togglable face shield
|
|
if ((itemTemplate?.Properties?.HasHinge ?? false) && (itemTemplate.Properties.FaceShieldComponent ?? false))
|
|
{
|
|
var faceShieldActiveChance = equipmentSettings?.FaceShieldIsActiveChancePercent ?? 75;
|
|
itemUpd.Togglable = new UpdTogglable { On = randomUtil.GetChance100(faceShieldActiveChance) };
|
|
hasProperties = true;
|
|
}
|
|
|
|
if (forceStackObjectsCount)
|
|
{
|
|
// Ensure property is set
|
|
itemUpd.StackObjectsCount ??= 1;
|
|
}
|
|
|
|
// Some items (weapon mods) may not have any props, and we don't want an empty Upd object
|
|
return hasProperties || forceStackObjectsCount ? itemUpd : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Choose a random value between a min and max for a resource to be
|
|
/// </summary>
|
|
/// <param name="maxResource">Max resource value of medical items</param>
|
|
/// <param name="randomizationValues">Value provided from config</param>
|
|
/// <returns>Randomized value from maxHpResource</returns>
|
|
protected double GetRandomizedResourceValue(double maxResource, RandomisedResourceValues? randomizationValues)
|
|
{
|
|
if (randomizationValues is null || randomUtil.GetChance100(randomizationValues.ChanceMaxResourcePercent))
|
|
{
|
|
return maxResource;
|
|
}
|
|
|
|
if (maxResource.Approx(1))
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
// Generate a randomised min value the resource could have
|
|
var min = Math.Max(1, randomUtil.GetPercentOfValue(randomizationValues.ResourcePercent, maxResource, 0));
|
|
|
|
// Choose value from randomised min and resource max possible
|
|
return randomUtil.GetDouble(min, maxResource);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get equipment specific flags (e.g. nvg settings) for a particular bot type
|
|
/// </summary>
|
|
/// <param name="botRole">bot to get settings for</param>
|
|
/// <returns>Equipment filter settings</returns>
|
|
protected EquipmentFilters? GetBotEquipmentSettingFromConfig(string botRole)
|
|
{
|
|
return BotConfig.Equipment.GetValueOrDefault(GetBotEquipmentRole(botRole));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a repairable object for a weapon that containers durability + max durability properties
|
|
/// </summary>
|
|
/// <param name="itemTemplate">weapon object being generated for</param>
|
|
/// <param name="botRole">type of bot being generated for</param>
|
|
/// <returns>Repairable object</returns>
|
|
protected UpdRepairable GenerateWeaponRepairableProperties(TemplateItem itemTemplate, string? botRole = null)
|
|
{
|
|
var maxDurability = durabilityLimitsHelper.GetRandomizedMaxWeaponDurability(botRole);
|
|
var currentDurability = durabilityLimitsHelper.GetRandomizedWeaponDurability(botRole, maxDurability);
|
|
|
|
return new UpdRepairable { Durability = Math.Round(currentDurability, 5), MaxDurability = Math.Round(maxDurability, 5) };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a repairable object for an armor that containers durability + max durability properties
|
|
/// </summary>
|
|
/// <param name="itemTemplate">weapon object being generated for</param>
|
|
/// <param name="botRole">type of bot being generated for</param>
|
|
/// <returns>Repairable object</returns>
|
|
protected UpdRepairable GenerateArmorRepairableProperties(TemplateItem itemTemplate, string? botRole = null)
|
|
{
|
|
double maxDurability;
|
|
double currentDurability;
|
|
if (itemTemplate.Properties?.ArmorClass == 0)
|
|
{
|
|
maxDurability = itemTemplate.Properties.MaxDurability.Value;
|
|
currentDurability = itemTemplate.Properties.MaxDurability.Value;
|
|
}
|
|
else
|
|
{
|
|
maxDurability = durabilityLimitsHelper.GetRandomizedMaxArmorDurability(itemTemplate, botRole);
|
|
currentDurability = durabilityLimitsHelper.GetRandomizedArmorDurability(itemTemplate, botRole, maxDurability);
|
|
}
|
|
|
|
return new UpdRepairable { Durability = Math.Round(currentDurability, 5), MaxDurability = Math.Round(maxDurability, 5) };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can item be added to another item without conflict
|
|
/// </summary>
|
|
/// <param name="itemsEquipped">Items to check compatibilities with</param>
|
|
/// <param name="tplToCheck">Tpl of the item to check for incompatibilities</param>
|
|
/// <param name="equipmentSlot">Slot the item will be placed into</param>
|
|
/// <returns>false if no incompatibilities, also has incompatibility reason</returns>
|
|
public ChooseRandomCompatibleModResult IsItemIncompatibleWithCurrentItems(
|
|
IEnumerable<Item> itemsEquipped,
|
|
MongoId tplToCheck,
|
|
string equipmentSlot
|
|
)
|
|
{
|
|
// Skip slots that have no incompatibilities
|
|
if (_slotsWithNoCompatIssues.Contains(equipmentSlot))
|
|
{
|
|
return new ChooseRandomCompatibleModResult
|
|
{
|
|
Incompatible = false,
|
|
Found = false,
|
|
Reason = string.Empty,
|
|
};
|
|
}
|
|
|
|
// TODO: Can probably be optimized to cache itemTemplates as items are added to inventory
|
|
var equippedItemsDb = itemsEquipped.Select(equippedItem => itemHelper.GetItem(equippedItem.Template).Value);
|
|
|
|
var (itemIsValid, itemToEquip) = itemHelper.GetItem(tplToCheck);
|
|
|
|
if (!itemIsValid)
|
|
{
|
|
logger.Warning(
|
|
serverLocalisationService.GetText(
|
|
"bot-invalid_item_compatibility_check",
|
|
new { itemTpl = tplToCheck, slot = equipmentSlot }
|
|
)
|
|
);
|
|
|
|
return new ChooseRandomCompatibleModResult
|
|
{
|
|
Incompatible = true,
|
|
Found = false,
|
|
Reason = $"item: {tplToCheck} does not exist in the database",
|
|
};
|
|
}
|
|
|
|
if (itemToEquip?.Properties is null)
|
|
{
|
|
logger.Warning(
|
|
serverLocalisationService.GetText(
|
|
"bot-compatibility_check_missing_props",
|
|
new
|
|
{
|
|
id = itemToEquip?.Id,
|
|
name = itemToEquip?.Name,
|
|
slot = equipmentSlot,
|
|
}
|
|
)
|
|
);
|
|
|
|
return new ChooseRandomCompatibleModResult
|
|
{
|
|
Incompatible = true,
|
|
Found = false,
|
|
Reason = $"item: {tplToCheck} does not have a _props field",
|
|
};
|
|
}
|
|
|
|
// Does an equipped item have a property that blocks the desired item - check for prop "BlocksX" .e.g BlocksEarpiece / BlocksFaceCover
|
|
var templateItems = equippedItemsDb;
|
|
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}`);
|
|
{
|
|
return new ChooseRandomCompatibleModResult
|
|
{
|
|
Incompatible = true,
|
|
Found = false,
|
|
Reason = $"{tplToCheck} {itemToEquip.Name} in slot: {equipmentSlot} blocked by: {blockingItem.Id} {blockingItem.Name}",
|
|
SlotBlocked = true,
|
|
};
|
|
}
|
|
|
|
// Check if any of the current inventory templates have the incoming item defined as incompatible
|
|
blockingItem = templateItems.FirstOrDefault(x => x?.Properties?.ConflictingItems?.Contains(tplToCheck) ?? false);
|
|
if (blockingItem is not null)
|
|
// this.logger.warning(`2 incompatibility found between - {itemToEquip[1]._name} and {blockingItem._props.Name} - {equipmentSlot}`);
|
|
{
|
|
return new ChooseRandomCompatibleModResult
|
|
{
|
|
Incompatible = true,
|
|
Found = false,
|
|
Reason = $"{tplToCheck} {itemToEquip.Name} in slot: {equipmentSlot} blocked by: {blockingItem.Id} {blockingItem.Name}",
|
|
SlotBlocked = true,
|
|
};
|
|
}
|
|
|
|
// Does item being checked get blocked/block existing item
|
|
if (itemToEquip.Properties.BlocksHeadwear ?? false)
|
|
{
|
|
var existingHeadwear = itemsEquipped.FirstOrDefault(x => x.SlotId == Containers.Headwear);
|
|
if (existingHeadwear is not null)
|
|
{
|
|
return new ChooseRandomCompatibleModResult
|
|
{
|
|
Incompatible = true,
|
|
Found = false,
|
|
Reason =
|
|
$"{tplToCheck} {itemToEquip.Name} is blocked by: {existingHeadwear.Template} in slot: {existingHeadwear.SlotId}",
|
|
SlotBlocked = true,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Does item being checked get blocked/block existing item
|
|
if (itemToEquip.Properties.BlocksFaceCover.GetValueOrDefault(false))
|
|
{
|
|
var existingFaceCover = itemsEquipped.FirstOrDefault(item => item.SlotId == Containers.FaceCover);
|
|
if (existingFaceCover is not null)
|
|
{
|
|
return new ChooseRandomCompatibleModResult
|
|
{
|
|
Incompatible = true,
|
|
Found = false,
|
|
Reason =
|
|
$"{tplToCheck} {itemToEquip.Name} is blocked by: {existingFaceCover.Template} in slot: {existingFaceCover.SlotId}",
|
|
SlotBlocked = true,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Does item being checked get blocked/block existing item
|
|
if (itemToEquip.Properties.BlocksEarpiece.GetValueOrDefault(false))
|
|
{
|
|
var existingEarpiece = itemsEquipped.FirstOrDefault(item => item.SlotId == Containers.Earpiece);
|
|
if (existingEarpiece is not null)
|
|
{
|
|
return new ChooseRandomCompatibleModResult
|
|
{
|
|
Incompatible = true,
|
|
Found = false,
|
|
Reason =
|
|
$"{tplToCheck} {itemToEquip.Name} is blocked by: {existingEarpiece.Template} in slot: {existingEarpiece.SlotId}",
|
|
SlotBlocked = true,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Does item being checked get blocked/block existing item
|
|
if (itemToEquip.Properties.BlocksArmorVest.GetValueOrDefault(false))
|
|
{
|
|
var existingArmorVest = itemsEquipped.FirstOrDefault(item => item.SlotId == Containers.ArmorVest);
|
|
if (existingArmorVest is not null)
|
|
{
|
|
return new ChooseRandomCompatibleModResult
|
|
{
|
|
Incompatible = true,
|
|
Found = false,
|
|
Reason =
|
|
$"{tplToCheck} {itemToEquip.Name} is blocked by: {existingArmorVest.Template} in slot: {existingArmorVest.SlotId}",
|
|
SlotBlocked = true,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check if the incoming item has any inventory items defined as incompatible
|
|
var blockingInventoryItem = itemsEquipped.FirstOrDefault(x =>
|
|
itemToEquip.Properties.ConflictingItems?.Contains(x.Template) ?? false
|
|
);
|
|
if (blockingInventoryItem is not null)
|
|
// this.logger.warning(`3 incompatibility found between - {itemToEquip[1]._name} and {blockingInventoryItem._tpl} - {equipmentSlot}`)
|
|
{
|
|
return new ChooseRandomCompatibleModResult
|
|
{
|
|
Incompatible = true,
|
|
Found = false,
|
|
Reason = $"{tplToCheck} blocks existing item {blockingInventoryItem.Template} in slot {blockingInventoryItem.SlotId}",
|
|
};
|
|
}
|
|
|
|
return new ChooseRandomCompatibleModResult { Incompatible = false, Reason = string.Empty };
|
|
}
|
|
|
|
protected bool HasBlockingProperty(TemplateItem? item, string blockingPropertyName)
|
|
{
|
|
return item != null && item.Blocks.TryGetValue(blockingPropertyName, out var blocks) && blocks;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert a bots role to the equipment role used in config/bot.json
|
|
/// </summary>
|
|
/// <param name="botRole">Role to convert</param>
|
|
/// <returns>Equipment role (e.g. pmc / assault / bossTagilla)</returns>
|
|
public string GetBotEquipmentRole(string botRole)
|
|
{
|
|
return _pmcTypes.Contains(botRole.ToLower()) ? Sides.PmcEquipmentRole : botRole;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an item with all its children into specified equipmentSlots, wherever it fits
|
|
/// </summary>
|
|
/// <param name="botId">Bots unique identifier</param>
|
|
/// <param name="equipmentSlots">Slot to try and add item+children into</param>
|
|
/// <param name="rootItemId">Root item id to use as mod items parentId</param>
|
|
/// <param name="rootItemTplId">Root items tpl id</param>
|
|
/// <param name="itemWithChildren">Item to add</param>
|
|
/// <param name="inventory">Inventory to add item+children into</param>
|
|
/// <returns>ItemAddedResult result object</returns>
|
|
public ItemAddedResult AddItemWithChildrenToEquipmentSlot(
|
|
MongoId botId,
|
|
HashSet<EquipmentSlots> equipmentSlots,
|
|
MongoId rootItemId,
|
|
MongoId rootItemTplId,
|
|
IEnumerable<Item> itemWithChildren,
|
|
BotBaseInventory inventory
|
|
)
|
|
{
|
|
var itemWithChildrenList = itemWithChildren.ToList();
|
|
|
|
// Track how many containers are unable to be found
|
|
var missingContainerCount = 0;
|
|
foreach (var equipmentSlotId in equipmentSlots)
|
|
{
|
|
// Get container from inventory to put item into
|
|
var container = inventory.Items?.FirstOrDefault(item => item.SlotId == equipmentSlotId.ToString());
|
|
if (container is null)
|
|
{
|
|
missingContainerCount++;
|
|
if (missingContainerCount == equipmentSlots.Count)
|
|
{
|
|
// Bot doesn't have any containers we want to add item to
|
|
if (logger.IsLogEnabled(LogLevel.Debug))
|
|
{
|
|
logger.Debug(
|
|
$"Unable to add item: {itemWithChildrenList.FirstOrDefault()?.Template} to bot as it lacks the following containers: {string.Join(",", equipmentSlots)}"
|
|
);
|
|
}
|
|
|
|
return ItemAddedResult.NO_CONTAINERS;
|
|
}
|
|
|
|
// No container of desired type found, skip to next container type
|
|
continue;
|
|
}
|
|
|
|
// Get container details from db
|
|
var (isValidItem, containerDbDetails) = itemHelper.GetItem(container.Template);
|
|
if (!isValidItem)
|
|
{
|
|
logger.Warning(serverLocalisationService.GetText("bot-missing_container_with_tpl", container.Template));
|
|
|
|
// Bad item, skip
|
|
continue;
|
|
}
|
|
|
|
if (containerDbDetails?.Properties?.Grids is null || !containerDbDetails.Properties.Grids.Any())
|
|
{
|
|
// Container has no slots to hold items, skip to next container
|
|
continue;
|
|
}
|
|
|
|
// Get x/y grid size of item
|
|
var (itemWidth, itemHeight) = inventoryHelper.GetItemSize(rootItemTplId, rootItemId, itemWithChildrenList);
|
|
|
|
var result = botInventoryContainerService.TryAddItemToBotContainer(
|
|
botId,
|
|
equipmentSlotId,
|
|
itemWithChildrenList,
|
|
inventory,
|
|
itemWidth,
|
|
itemHeight
|
|
);
|
|
if (result != ItemAddedResult.SUCCESS)
|
|
{
|
|
// Failed to add to container, try next
|
|
continue;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
return ItemAddedResult.NO_SPACE;
|
|
}
|
|
}
|