diff --git a/Core/Controllers/BotController.cs b/Core/Controllers/BotController.cs index 913205d4..f73db1ef 100644 --- a/Core/Controllers/BotController.cs +++ b/Core/Controllers/BotController.cs @@ -315,7 +315,7 @@ public class BotController _matchBotDetailsCacheService.CacheBot(botToCache); } - private void UpdateBotGenerationDetailsToRandomBoss(BotGenerationDetails botGenerationDetails, Dictionary bossesToConvertToWeights) + private void UpdateBotGenerationDetailsToRandomBoss(BotGenerationDetails botGenerationDetails, Dictionary 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); diff --git a/Core/Generators/BotGenerator.cs b/Core/Generators/BotGenerator.cs index d18486d4..249fc9fb 100644 --- a/Core/Generators/BotGenerator.cs +++ b/Core/Generators/BotGenerator.cs @@ -146,7 +146,7 @@ public class BotGenerator WishList = bot.WishList, MoneyTransferLimitData = bot.MoneyTransferLimitData, IsPmc = bot.IsPmc, - Prestige = new Prestige() + Prestige = new Dictionary() }; } @@ -435,9 +435,9 @@ public class BotGenerator public void AddAdditionalPocketLootWeightsForUnheardBot(BotType botJsonTemplate) { // Adjust pocket loot weights to allow for 5 or 6 items - var pocketWeights = botJsonTemplate.BotGeneration.Items["pocketLoot"].Weights; - pocketWeights["5"] = 1; - pocketWeights["6"] = 1; + var pocketWeights = botJsonTemplate.BotGeneration.Items.PocketLoot.Weights; + pocketWeights[5] = 1; + pocketWeights[6] = 1; } /// @@ -496,9 +496,9 @@ public class BotGenerator var chosenBodyTemplate = _databaseService.GetCustomization()[bot.Customization.Body]; // Some bodies have matching hands, look up body to see if this is the case - var chosenBody = bodyGlobalDictDb[chosenBodyTemplate?.Name.Trim()]; - bot.Customization.Hands = chosenBody?.IsNotRandom ?? false - ? chosenBody.Hands // Has fixed hands for chosen body, update to match + var chosenBody = bodyGlobalDictDb.FirstOrDefault(c => c.Key == chosenBodyTemplate?.Name.Trim()); + bot.Customization.Hands = chosenBody.Value.IsNotRandom ?? false + ? chosenBody.Value.Hands // Has fixed hands for chosen body, update to match : _weightedRandomHelper.GetWeightedValue(appearance.Hands); // Hands can be random, choose any from weighted dict } diff --git a/Core/Generators/BotInventoryGenerator.cs b/Core/Generators/BotInventoryGenerator.cs index dfe45b13..f243b22a 100644 --- a/Core/Generators/BotInventoryGenerator.cs +++ b/Core/Generators/BotInventoryGenerator.cs @@ -145,7 +145,8 @@ public class BotInventoryGenerator var questStashItemsId = _hashUtil.Generate(); var sortingTableId = _hashUtil.Generate(); - return new BotBaseInventory{ + return new BotBaseInventory + { Items = [ new() { Id = equipmentId, Template = ItemTpl.INVENTORY_DEFAULT }, @@ -181,7 +182,8 @@ public class BotInventoryGenerator BotBaseInventory botInventory, int botLevel, string chosenGameVersion, GetRaidConfigurationRequestData raidConfig) { // These will be handled later - var excludedSlots = new List(){ + var excludedSlots = new List() + { EquipmentSlots.Pockets, EquipmentSlots.FirstPrimaryWeapon, EquipmentSlots.SecondPrimaryWeapon, @@ -203,10 +205,11 @@ public class BotInventoryGenerator _weatherHelper.IsNightTime(raidConfig.TimeVariant) ) { - foreach (var equipmentSlotKvP in (randomistionDetails.NighttimeChanges.EquipmentModsModifiers)) { + 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); + Math.Max(randomistionDetails.EquipmentMods[equipmentSlotKvP.Key] + equipmentSlotKvP.Value, 0), 100); } } @@ -218,14 +221,16 @@ public class BotInventoryGenerator // 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 equipmentSlotKvP in templateInventory.Equipment) { + foreach (var equipmentSlotKvP 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(equipmentSlotKvP.Key)) { + if (excludedSlots.Contains(equipmentSlotKvP.Key)) + { continue; } - GenerateEquipment( new GenerateEquipmentProperties + GenerateEquipment(new GenerateEquipmentProperties { RootEquipmentSlot = equipmentSlotKvP.Key, RootEquipmentPool = equipmentSlotKvP.Value, @@ -240,17 +245,17 @@ public class BotInventoryGenerator } // Generate below in specific order - GenerateEquipment( new GenerateEquipmentProperties + 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{ [ItemTpl.POCKETS_1X4_TUE] = 1 } + ? new Dictionary { [ItemTpl.POCKETS_1X4_TUE] = 1 } : templateInventory.Equipment[EquipmentSlots.Pockets], ModPool = templateInventory.Mods, SpawnChances = wornItemChances, - BotData = new BotData{ Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole }, + BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole }, Inventory = botInventory, BotEquipmentConfig = botEquipConfig, RandomisationDetails = randomistionDetails, @@ -258,7 +263,7 @@ public class BotInventoryGenerator GeneratingPlayerLevel = pmcProfile.Info.Level, }); - GenerateEquipment( new GenerateEquipmentProperties + GenerateEquipment(new GenerateEquipmentProperties { RootEquipmentSlot = EquipmentSlots.FaceCover, RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.FaceCover], @@ -271,7 +276,7 @@ public class BotInventoryGenerator GeneratingPlayerLevel = pmcProfile.Info.Level, }); - GenerateEquipment( new GenerateEquipmentProperties + GenerateEquipment(new GenerateEquipmentProperties { RootEquipmentSlot = EquipmentSlots.Headwear, RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Headwear], @@ -297,7 +302,7 @@ public class BotInventoryGenerator GeneratingPlayerLevel = pmcProfile.Info.Level, }); - var hasArmorVest = GenerateEquipment( new GenerateEquipmentProperties + var hasArmorVest = GenerateEquipment(new GenerateEquipmentProperties { RootEquipmentSlot = EquipmentSlots.ArmorVest, RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.ArmorVest], @@ -311,23 +316,26 @@ public class BotInventoryGenerator }); // Bot has no armor vest and flagged to be forced to wear armored rig in this event - if (botEquipConfig.ForceOnlyArmoredRigWhenNoArmor.GetValueOrDefault(false) && !hasArmorVest) { + 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) { + 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) { + if (botEquipConfig.ForceRigWhenNoVest.GetValueOrDefault(false) && !hasArmorVest) + { wornItemChances.EquipmentChances["TacticalVest"] = 100; } - GenerateEquipment( new GenerateEquipmentProperties + GenerateEquipment(new GenerateEquipmentProperties { RootEquipmentSlot = EquipmentSlots.Earpiece, RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Earpiece], @@ -348,12 +356,17 @@ public class BotInventoryGenerator /// Role of bot vests are being filtered for public void FilterRigsToThoseWithProtection(Dictionary> templateEquipment, string botRole) { - throw new NotImplementedException(); - } - - public void FilterRigsToThoseWithoutProtection(Dictionary> templateEquipment, string botRole, bool allowEmptyResult = true) - { - throw new NotImplementedException(); + var tacVestsWithArmor = templateEquipment[EquipmentSlots.TacticalVest].Where(kvp => _itemHelper.ItemHasSlots(kvp.Key)) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + if (tacVestsWithArmor.Count() == 0) + { + _logger.Debug($"Unable to filter to only armored rigs as bot: {botRole} has none in pool"); + + return; + } + + templateEquipment[EquipmentSlots.TacticalVest] = tacVestsWithArmor; } /// @@ -362,9 +375,20 @@ public class BotInventoryGenerator /// Equpiment to filter TacticalVest of /// Role of bot vests are being filtered for /// Should the function return all rigs when 0 unarmored are found - public void FilterRigsTothoseWithoutProtection(Equipment templateEquipment, string botRole, bool allowEmptyRequest = false) + public void FilterRigsToThoseWithoutProtection(Dictionary> templateEquipment, string botRole, + bool allowEmptyResult = true) { - throw new NotImplementedException(); + var tacVestsWithoutArmor = templateEquipment[EquipmentSlots.TacticalVest].Where(kvp => !_itemHelper.ItemHasSlots(kvp.Key)) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + if (!allowEmptyResult && tacVestsWithoutArmor.Count() == 0) + { + _logger.Debug($"Unable to filter to only unarmored rigs as bot: {botRole} has none in pool"); + + return; + } + + templateEquipment[EquipmentSlots.TacticalVest] = tacVestsWithoutArmor; } /// @@ -375,7 +399,129 @@ public class BotInventoryGenerator public bool GenerateEquipment(GenerateEquipmentProperties settings) { _logger.Error("NOT IMPLEMENTED - GenerateEquipment"); - return true; + List slotsToCheck = [EquipmentSlots.Pockets.ToString(), EquipmentSlots.SecuredContainer.ToString()]; + double? spawnChance = slotsToCheck.Contains(settings.RootEquipmentSlot.ToString()) + ? 100 + : settings.SpawnChances.EquipmentChances[settings.RootEquipmentSlot.ToString()]; + + if (spawnChance is null) + { + _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.Count() == 0) + { + TemplateItem pickedItemDb = new TemplateItem(); + var found = false; + + // Limit attempts to find a compatible item as its 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.Count() == 0) + { + 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?.Count() > 0 && (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; } /// @@ -386,7 +532,19 @@ public class BotInventoryGenerator /// Filtered pool of mods public Dictionary> GetFilteredDynamicModsForItem(string itemTpl, Dictionary> equipmentBlacklist) { - throw new NotImplementedException(); + var modPool = _botEquipmentModPoolService.GetModsForGearSlot(itemTpl); + foreach (var modSlot in modPool.Keys ?? Enumerable.Empty()) + { + var blacklistedMods = equipmentBlacklist[modSlot] ?? []; + var filteredMods = modPool[modSlot].Where((slotName) => !blacklistedMods.Contains(slotName)); + + if (filteredMods.Count() > 0) + { + modPool[modSlot] = filteredMods.ToList(); + } + } + + return modPool; } /// @@ -401,10 +559,27 @@ public class BotInventoryGenerator /// Limits for items the bot can have /// level of bot having weapon generated public void GenerateAndAddWeaponsToBot(BotTypeInventory templateInventory, Chances equipmentChances, string sessionId, BotBaseInventory botInventory, - string botRole, - bool isPmc, Generation itemGenerationLimitsMinMax, int botLevel) + string botRole, bool isPmc, Generation itemGenerationLimitsMinMax, int botLevel) { - throw new NotImplementedException(); + var weaponSlotsToFill = GetDesiredWeaponsForBot(equipmentChances); + foreach (var weaponSlot in weaponSlotsToFill) + { + // Add weapon to bot if true and bot json has something to put into the slot + if (weaponSlot.ShouldSpawn && templateInventory.Equipment[weaponSlot.Slot].Any()) + { + AddWeaponAndMagazinesToInventory( + sessionId, + weaponSlot, + templateInventory, + botInventory, + equipmentChances, + botRole, + isPmc, + itemGenerationLimitsMinMax, + botLevel + ); + } + } } /// @@ -412,9 +587,30 @@ public class BotInventoryGenerator /// /// Chances bot has certain equipment /// What slots bot should have weapons generated for - public object GetDesiredWeaponsForBot(Chances equipmentChances) // TODO: Type fuckery { slot: EquipmentSlots; shouldSpawn: boolean }[] + public List GetDesiredWeaponsForBot(Chances equipmentChances) // TODO: Type fuckery { slot: EquipmentSlots; shouldSpawn: boolean }[] { - throw new NotImplementedException(); + 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 + } + ]; } /// @@ -429,10 +625,34 @@ public class BotInventoryGenerator /// Is the bot being generated as a pmc /// /// - public void AddWeaponAndMagazineToInventory(string sessionId, object weaponSlot, BotBaseInventory templateInventory, BotBaseInventory botInventory, + public void AddWeaponAndMagazinesToInventory(string sessionId, DesiredWeapons weaponSlot, BotTypeInventory templateInventory, BotBaseInventory botInventory, Chances equipmentChances, string botRole, bool isPmc, Generation itemGenerationWeights, int botLevel) { - throw new NotImplementedException(); + 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; } +} diff --git a/Core/Generators/BotLootGenerator.cs b/Core/Generators/BotLootGenerator.cs index 63b01612..9cabb240 100644 --- a/Core/Generators/BotLootGenerator.cs +++ b/Core/Generators/BotLootGenerator.cs @@ -1,19 +1,74 @@ -using Core.Annotations; +using Core.Annotations; using Core.Models.Eft.Common.Tables; using Core.Models.Enums; using Core.Models.Spt.Bots; using Core.Models.Spt.Config; +using Core.Utils; +using Core.Helpers; +using Core.Services; +using Core.Servers; +using Core.Utils.Cloners; +using ILogger = Core.Models.Utils.ILogger; namespace Core.Generators; [Injectable] public class BotLootGenerator { + private readonly ILogger _logger; + private readonly HashUtil _hashUtil; + private readonly RandomUtil _randomUtil; + private readonly ItemHelper _itemHelper; + private readonly InventoryHelper _inventoryHelper; + private readonly DatabaseService _databaseService; + private readonly HandbookHelper _handbookHelper; + private readonly BotGeneratorHelper _botGeneratorHelper; + private readonly BotWeaponGenerator _botWeaponGenerator; + private readonly WeightedRandomHelper _weightedRandomHelper; + private readonly BotHelper _botHelper; + private readonly BotLootCacheService _botLootCacheService; + private readonly LocalisationService _localisationService; + private readonly ConfigServer _configServer; + private readonly ICloner _cloner; + private BotConfig _botConfig; private PmcConfig _pmcConfig; - public BotLootGenerator() + public BotLootGenerator( + ILogger logger, + HashUtil hashUtil, + RandomUtil randomUtil, + ItemHelper itemHelper, + InventoryHelper inventoryHelper, + DatabaseService databaseService, + HandbookHelper handbookHelper, + BotGeneratorHelper botGeneratorHelper, + BotWeaponGenerator botWeaponGenerator, + WeightedRandomHelper weightedRandomHelper, + BotHelper botHelper, + BotLootCacheService botLootCacheService, + LocalisationService localisationService, + ConfigServer configServer, + ICloner cloner) { + _logger = logger; + _hashUtil = hashUtil; + _randomUtil = randomUtil; + _itemHelper = itemHelper; + _inventoryHelper = inventoryHelper; + _databaseService = databaseService; + _handbookHelper = handbookHelper; + _botGeneratorHelper = botGeneratorHelper; + _botWeaponGenerator = botWeaponGenerator; + _weightedRandomHelper = weightedRandomHelper; + _botHelper = botHelper; + _botLootCacheService = botLootCacheService; + _localisationService = localisationService; + _configServer = configServer; + _cloner = cloner; + + _botConfig = _configServer.GetConfig(); + _pmcConfig = _configServer.GetConfig(); } /// @@ -38,7 +93,261 @@ public class BotLootGenerator /// Level of bot public void GenerateLoot(string sessionId, BotType botJsonTemplate, bool isPmc, string botRole, BotBaseInventory botInventory, int botLevel) { - throw new NotImplementedException(); + // Limits on item types to be added as loot + var itemCounts = botJsonTemplate.BotGeneration.Items; + + if ( + itemCounts.BackpackLoot.Weights is null || + itemCounts.PocketLoot.Weights is null || + itemCounts.VestLoot.Weights is null || + itemCounts.SpecialItems.Weights is null || + itemCounts.Healing.Weights is null || + itemCounts.Drugs.Weights is null || + itemCounts.Food.Weights is null || + itemCounts.Drink.Weights is null || + itemCounts.Currency.Weights is null || + itemCounts.Stims.Weights is null || + itemCounts.Grenades.Weights is null + ) + { + _logger.Warning(_localisationService.GetText("bot-unable_to_generate_bot_loot", botRole)); + return; + } + var backpackLootCount = _weightedRandomHelper.GetWeightedValue(itemCounts.BackpackLoot.Weights); + var pocketLootCount = _weightedRandomHelper.GetWeightedValue(itemCounts.PocketLoot.Weights); + var vestLootCount = _weightedRandomHelper.GetWeightedValue(itemCounts.VestLoot.Weights); + var specialLootItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.SpecialItems.Weights); + var healingItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Healing.Weights); + var drugItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Drugs.Weights); + var foodItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Food.Weights); + var drinkItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Drink.Weights); + var currencyItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Currency.Weights); + var stimItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Stims.Weights); + var grenadeCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Grenades.Weights); + + // If bot has been flagged as not having loot, set below counts to 0 + if (_botConfig.DisableLootOnBotTypes.Contains(botRole.ToLower())) + { + backpackLootCount = 0; + pocketLootCount = 0; + vestLootCount = 0; + currencyItemCount = 0; + } + + // Forced pmc healing loot into secure container + if (isPmc && _pmcConfig.ForceHealingItemsIntoSecure) + { + AddForcedMedicalItemsToPmcSecure(botInventory, botRole); + } + + var botItemLimits = GetItemSpawnLimitsForBot(botRole); + + var containersBotHasAvailable = GetAvailableContainersBotCanStoreItemsIn(botInventory); + + // This set is passed as a reference to fill up the containers that are already full, this alleviates + // generation of the bots by avoiding checking the slots of containers we already know are full + var containersIdFull = new List(); + + // Special items + AddLootFromPool( + _botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.Special, botJsonTemplate), + containersBotHasAvailable, + specialLootItemCount, + botInventory, + botRole, + botItemLimits, + containersIdFull); + + // Healing items / Meds + AddLootFromPool( + _botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.HealingItems, botJsonTemplate), + containersBotHasAvailable, + healingItemCount, + botInventory, + botRole, + null, + containersIdFull, + 0, + isPmc); + + // Drugs + AddLootFromPool( + _botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.DrugItems, botJsonTemplate), + containersBotHasAvailable, + drugItemCount, + botInventory, + botRole, + null, + containersIdFull, + 0, + isPmc); + + // Food + AddLootFromPool( + _botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.FoodItems, botJsonTemplate), + containersBotHasAvailable, + foodItemCount, + botInventory, + botRole, + null, + containersIdFull, + 0, + isPmc); + + // Drink + AddLootFromPool( + _botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.DrinkItems, botJsonTemplate), + containersBotHasAvailable, + drinkItemCount, + botInventory, + botRole, + null, + containersIdFull, + 0, + isPmc); + + // Currency + AddLootFromPool( + _botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.CurrencyItems, botJsonTemplate), + containersBotHasAvailable, + currencyItemCount, + botInventory, + botRole, + null, + containersIdFull, + 0, + isPmc); + + // Stims + AddLootFromPool( + _botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.StimItems, botJsonTemplate), + containersBotHasAvailable, + stimItemCount, + botInventory, + botRole, + botItemLimits, + containersIdFull, + 0, + isPmc); + + // Grenades + AddLootFromPool( + _botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.GrenadeItems, botJsonTemplate), + [EquipmentSlots.Pockets, EquipmentSlots.TacticalVest], // Can't use containersBotHasEquipped as we don't want grenades added to backpack + grenadeCount, + botInventory, + botRole, + null, + containersIdFull, + 0, + isPmc); + + var itemPriceLimits = GetSingleItemLootPriceLimits(botLevel, isPmc); + + // Backpack - generate loot if they have one + if (containersBotHasAvailable.Contains(EquipmentSlots.Backpack)) + { + // Add randomly generated weapon to PMC backpacks + if (isPmc && _randomUtil.GetChance100(_pmcConfig.LooseWeaponInBackpackChancePercent)) + { + AddLooseWeaponsToInventorySlot( + sessionId, + botInventory, + EquipmentSlots.Backpack, + botJsonTemplate.BotInventory, + botJsonTemplate.BotChances.WeaponModsChances, + botRole, + isPmc, + botLevel, + containersIdFull); + } + + var backpackLootRoubleTotal = GetBackpackRoubleTotalByLevel(botLevel, isPmc); + AddLootFromPool( + _botLootCacheService.GetLootFromCache( + botRole, + isPmc, + LootCacheType.Backpack, + botJsonTemplate, + itemPriceLimits?.Backpack), + [EquipmentSlots.Backpack], + backpackLootCount, + botInventory, + botRole, + botItemLimits, + containersIdFull, + backpackLootRoubleTotal, + isPmc); + } + + // TacticalVest - generate loot if they have one + if (containersBotHasAvailable.Contains(EquipmentSlots.TacticalVest)) + { + // Vest + AddLootFromPool( + _botLootCacheService.GetLootFromCache( + botRole, + isPmc, + LootCacheType.Vest, + botJsonTemplate, + itemPriceLimits?.Vest), + [EquipmentSlots.TacticalVest], + vestLootCount, + botInventory, + botRole, + botItemLimits, + containersIdFull, + _pmcConfig.MaxVestLootTotalRub, + isPmc); + } + + // Pockets + AddLootFromPool( + _botLootCacheService.GetLootFromCache( + botRole, + isPmc, + LootCacheType.Pocket, + botJsonTemplate, + itemPriceLimits?.Pocket), + [EquipmentSlots.Pockets], + pocketLootCount, + botInventory, + botRole, + botItemLimits, + containersIdFull, + _pmcConfig.MaxPocketLootTotalRub, + isPmc); + + // Secure + + // only add if not a pmc or is pmc and flag is true + if (!isPmc || (isPmc && _pmcConfig.AddSecureContainerLootFromBotConfig)) + { + AddLootFromPool( + _botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.Secure, botJsonTemplate), + [EquipmentSlots.SecuredContainer], + 50, + botInventory, + botRole, + null, + containersIdFull, + - 1, + isPmc); + } + } + + private MinMaxLootItemValue? GetSingleItemLootPriceLimits(int botLevel, bool isPmc) + { + // TODO - extend to other bot types + if (isPmc) + { + var matchingValue = _pmcConfig.LootItemLimitsRub.FirstOrDefault( + (minMaxValue) => botLevel >= minMaxValue.Min && botLevel <= minMaxValue.Max); + + return matchingValue; + } + + return null; } /// @@ -87,9 +396,16 @@ public class BotLootGenerator /// Total value of loot allowed in roubles /// Is bot being generated for a pmc /// - public void AddLootFromPool(Dictionary pool, List equipmentSlots, int totalItemCount, + public void AddLootFromPool( + Dictionary pool, + List equipmentSlots, + int totalItemCount, BotBaseInventory inventoryToAddItemsTo, // TODO: type for containersIdFull was Set - string botRole, ItemSpawnLimitSettings? itemSpawnLimits, List containersIdFull, int totalValueLimitRub = 0, bool isPmc = false) + string botRole, + ItemSpawnLimitSettings itemSpawnLimits, + List containersIdFull, + int totalValueLimitRub = 0, + bool isPmc = false) { throw new NotImplementedException(); } @@ -129,9 +445,15 @@ public class BotLootGenerator /// are we generating for a pmc /// /// - public void AddLooseWeaponsToInventorySlot(string sessionId, BotBaseInventory botInventory, string equipmentSlot, - BotBaseInventory templateInventory, // TODO: type for containersIdFull was Set - Dictionary modsChances, string botRole, bool isPmc, int botLevel, List? containersIdFull) + public void AddLooseWeaponsToInventorySlot(string sessionId, + BotBaseInventory botInventory, + EquipmentSlots equipmentSlot, + BotTypeInventory templateInventory, + Dictionary modsChances, + string botRole, + bool isPmc, + int botLevel, + List? containersIdFull) // TODO: type for containersIdFull was Set { throw new NotImplementedException(); } diff --git a/Core/Generators/BotWeaponGenerator.cs b/Core/Generators/BotWeaponGenerator.cs index 3f9f1c03..88a66c1e 100644 --- a/Core/Generators/BotWeaponGenerator.cs +++ b/Core/Generators/BotWeaponGenerator.cs @@ -29,7 +29,7 @@ public class BotWeaponGenerator /// Is weapon generated for a pmc /// /// GenerateWeaponResult object - public GenerateWeaponResult GenerateRandomWeapon(string sessionId, string equipmentSlot, BotBaseInventory botTemplateInventory, string weaponParentId, + public GenerateWeaponResult GenerateRandomWeapon(string sessionId, string equipmentSlot, BotTypeInventory botTemplateInventory, string weaponParentId, Dictionary modChances, string botRole, bool isPmc, int botLevel) { throw new NotImplementedException(); @@ -41,7 +41,7 @@ public class BotWeaponGenerator /// Primary/secondary/holster /// e.g. assault.json /// Weapon template - public string PickWeightedWeaponTemplateFromPool(string equipmentSlot, BotBaseInventory botTemplateInventory) + public string PickWeightedWeaponTemplateFromPool(string equipmentSlot, BotTypeInventory botTemplateInventory) { throw new NotImplementedException(); } diff --git a/Core/Generators/PMCLootGenerator.cs b/Core/Generators/PMCLootGenerator.cs index 5443d142..2e0748f6 100644 --- a/Core/Generators/PMCLootGenerator.cs +++ b/Core/Generators/PMCLootGenerator.cs @@ -1,4 +1,4 @@ -using Core.Annotations; +using Core.Annotations; using Core.Models.Eft.Common.Tables; namespace Core.Generators; @@ -15,7 +15,7 @@ public class PMCLootGenerator /// /// /// Dictionary of string and number - public Dictionary GeneratePMCPocketLootPool(string botRole) + public Dictionary GeneratePMCPocketLootPool(string botRole) { throw new NotImplementedException(); } @@ -25,7 +25,7 @@ public class PMCLootGenerator /// /// /// Dictionary of string and number - public Dictionary GeneratePMCVestLootPool(string botRole) + public Dictionary GeneratePMCVestLootPool(string botRole) { throw new NotImplementedException(); } @@ -57,7 +57,7 @@ public class PMCLootGenerator /// /// /// Dictionary of string and number - public Dictionary GeneratePMCBackpackLootPool(string botRole) + public Dictionary GeneratePMCBackpackLootPool(string botRole) { throw new NotImplementedException(); } diff --git a/Core/Generators/PlayerScavGenerator.cs b/Core/Generators/PlayerScavGenerator.cs index 3f8fe537..13a9d8f4 100644 --- a/Core/Generators/PlayerScavGenerator.cs +++ b/Core/Generators/PlayerScavGenerator.cs @@ -279,9 +279,11 @@ public class PlayerScavGenerator } // Adjust item spawn quantity values + var props = baseBotNode.BotGeneration.Items.GetType().GetProperties(); foreach (var itemLimitKvP in karmaSettings.ItemLimits) { - baseBotNode.BotGeneration.Items[itemLimitKvP.Key] = itemLimitKvP.Value; + var prop = props.FirstOrDefault(x => x.Name.ToLower() == itemLimitKvP.Key.ToLower()); + prop.SetValue(baseBotNode.BotGeneration.Items, itemLimitKvP.Value); } // Blacklist equipment, keyed by equipment slot diff --git a/Core/Generators/WeatherGenerator.cs b/Core/Generators/WeatherGenerator.cs index 1e8c14ef..a05ae751 100644 --- a/Core/Generators/WeatherGenerator.cs +++ b/Core/Generators/WeatherGenerator.cs @@ -159,7 +159,7 @@ public class WeatherGenerator var formattedDate = _timeUtil.FormatDate(timestamp.HasValue ? _timeUtil.GetDateTimeFromTimeStamp(timestamp.Value) : DateTime.UtcNow); var datetimeBsgFormat = $"{formattedDate} {normalTime}"; - weather.Timestamp = timestamp ?? _timeUtil.GetTimeStampFromEpoch(inRaidTime); // matches weather.date We use to divide by 1000 + weather.Timestamp = timestamp ?? _timeUtil.GetTimeStampFromEpoch(inRaidTime) / 1000; // matches weather.date weather.Date = formattedDate; // matches weather.timestamp weather.Time = datetimeBsgFormat; // matches weather.timestamp weather.SptInRaidTimestamp = _timeUtil.GetTimeStampFromEpoch(inRaidTime); diff --git a/Core/Helpers/BotGeneratorHelper.cs b/Core/Helpers/BotGeneratorHelper.cs index 829e7c49..a25f7472 100644 --- a/Core/Helpers/BotGeneratorHelper.cs +++ b/Core/Helpers/BotGeneratorHelper.cs @@ -1,6 +1,7 @@ using Core.Annotations; using Core.Models.Eft.Common.Tables; using Core.Models.Enums; +using Core.Models.Spt.Bots; using Core.Models.Spt.Config; using Core.Servers; @@ -84,7 +85,7 @@ public class BotGeneratorHelper /// Tpl of the item to check for incompatibilities /// Slot the item will be placed into /// false if no incompatibilities, also has incompatibility reason - public object IsItemIncompatibleWithCurrentItems(List itemsEquipped, string tplToCheck, string equipmentSlot) + public ChooseRandomCompatibleModResult IsItemIncompatibleWithCurrentItems(List itemsEquipped, string tplToCheck, string equipmentSlot) { throw new NotImplementedException(); } diff --git a/Core/Helpers/ItemHelper.cs b/Core/Helpers/ItemHelper.cs index 5142ccad..9fd605ef 100644 --- a/Core/Helpers/ItemHelper.cs +++ b/Core/Helpers/ItemHelper.cs @@ -1045,7 +1045,12 @@ public class ItemHelper throw new NotImplementedException(); } - public bool IsOfBaseclass(string tpl, List baseClassTpls) + public bool IsOfBaseclass(string tpl, string baseClassTpl) + { + return _itemBaseClassService.ItemHasBaseClass(tpl, [baseClassTpl]); + } + + public bool isOfBaseclasses(string tpl, List baseClassTpls) { return _itemBaseClassService.ItemHasBaseClass(tpl, baseClassTpls); } diff --git a/Core/Helpers/PresetHelper.cs b/Core/Helpers/PresetHelper.cs index 950eb063..08bee62d 100644 --- a/Core/Helpers/PresetHelper.cs +++ b/Core/Helpers/PresetHelper.cs @@ -57,7 +57,7 @@ public class PresetHelper var tempPresets = _databaseService.GetGlobals().ItemPresets; tempPresets = tempPresets.Where(p => p.Value.Encyclopedia != null && - _itemHelper.IsOfBaseclass(p.Value.Encyclopedia, [BaseClasses.WEAPON])).ToDictionary(); + _itemHelper.IsOfBaseclass(p.Value.Encyclopedia, BaseClasses.WEAPON)).ToDictionary(); } return _defaultWeaponPresets; diff --git a/Core/Helpers/WeatherHelper.cs b/Core/Helpers/WeatherHelper.cs index b434cce2..cff97e08 100644 --- a/Core/Helpers/WeatherHelper.cs +++ b/Core/Helpers/WeatherHelper.cs @@ -43,7 +43,7 @@ public class WeatherHelper var twentyFourHoursMilliseconds = _timeUtil.GetHoursAsSeconds(24) * 1000; var currentTimestampMilliSeconds = timestamp.HasValue ? timestamp ?? 0 - : (DateTime.UtcNow - DateTime.UnixEpoch).TotalMilliseconds; + : _timeUtil.GetTimeStampFromEpoch(); return _timeUtil.GetDateTimeFromTimeStamp((long) (russiaOffsetMilliseconds + currentTimestampMilliSeconds * _weatherConfig.Acceleration) % diff --git a/Core/Helpers/WeightedRandomHelper.cs b/Core/Helpers/WeightedRandomHelper.cs index 8c9a35b1..26da7265 100644 --- a/Core/Helpers/WeightedRandomHelper.cs +++ b/Core/Helpers/WeightedRandomHelper.cs @@ -22,7 +22,7 @@ public class WeightedRandomHelper /// /// Items and weights to use /// Chosen item from array - public T GetWeightedValue(Dictionary values) where T : notnull + public T GetWeightedValue(Dictionary values) where T : notnull { var itemKeys = values.Keys.ToList(); var weights = values.Values.ToList(); @@ -30,7 +30,6 @@ public class WeightedRandomHelper var chosenItem = WeightedRandom(itemKeys, weights); return chosenItem.Item; - // SORRY IF THIS BLEW UP, I DONT SEE A REASON ITS GENERIC - CWX } /// @@ -47,7 +46,7 @@ public class WeightedRandomHelper /// List of items /// List of weights /// Dictionary with item and index - public WeightedRandomResult WeightedRandom(List items, List weights) + public WeightedRandomResult WeightedRandom(List items, List weights) { if (items.Count == 0) { @@ -68,7 +67,7 @@ public class WeightedRandomHelper List cumulativeWeights = []; for (var i = 0; i < weights.Count; i++) { - cumulativeWeights.Add(weights[i] + (i > 0 ? cumulativeWeights[i - 1] : 0)); + cumulativeWeights.Add((int)(weights[i]) + (i > 0 ? (cumulativeWeights[i - 1]) : 0)); } // Getting the random number in a range of [0...sum(weights)] diff --git a/Core/Models/Eft/Common/PmcData.cs b/Core/Models/Eft/Common/PmcData.cs index 6e7b1ffd..8f240e00 100644 --- a/Core/Models/Eft/Common/PmcData.cs +++ b/Core/Models/Eft/Common/PmcData.cs @@ -8,7 +8,7 @@ public class PmcData : BotBase { [JsonPropertyName("Prestige")] [JsonConverter(typeof(ArrayToObjectFactoryConverter))] - public Tables.Prestige? Prestige { get; set; } + public Dictionary? Prestige { get; set; } } public class PostRaidPmcData : BotBase diff --git a/Core/Models/Eft/Common/Tables/BotType.cs b/Core/Models/Eft/Common/Tables/BotType.cs index 180b1487..ccc92bd2 100644 --- a/Core/Models/Eft/Common/Tables/BotType.cs +++ b/Core/Models/Eft/Common/Tables/BotType.cs @@ -41,22 +41,22 @@ public class BotType public class Appearance { [JsonPropertyName("body")] - public Dictionary? Body { get; set; } + public Dictionary? Body { get; set; } [JsonPropertyName("feet")] - public Dictionary? Feet { get; set; } + public Dictionary? Feet { get; set; } [JsonPropertyName("hands")] [JsonConverter(typeof(ArrayToObjectFactoryConverter))] - public Dictionary? Hands { get; set; } + public Dictionary? Hands { get; set; } [JsonPropertyName("head")] [JsonConverter(typeof(ArrayToObjectFactoryConverter))] - public Dictionary? Head { get; set; } + public Dictionary? Head { get; set; } [JsonPropertyName("voice")] [JsonConverter(typeof(ArrayToObjectFactoryConverter))] - public Dictionary? Voice { get; set; } + public Dictionary? Voice { get; set; } } public class Chances @@ -296,14 +296,14 @@ public class Experience public class Generation { [JsonPropertyName("items")] - public Dictionary? Items { get; set; } + public GenerationWeightingItems? Items { get; set; } } public class GenerationData { /** key: number of items, value: weighting */ [JsonPropertyName("weights")] - public Dictionary? Weights { get; set; } + public Dictionary? Weights { get; set; } /** Array of item tpls */ [JsonPropertyName("whitelist")] @@ -311,6 +311,48 @@ public class GenerationData public Dictionary? Whitelist { get; set; } } +public class GenerationWeightingItems +{ + [JsonPropertyName("grenades")] + public GenerationData Grenades { get; set; } + + [JsonPropertyName("healing")] + public GenerationData Healing { get; set; } + + [JsonPropertyName("drugs")] + public GenerationData Drugs { get; set; } + + [JsonPropertyName("food")] + public GenerationData Food { get; set; } + + [JsonPropertyName("drink")] + public GenerationData Drink { get; set; } + + [JsonPropertyName("currency")] + public GenerationData Currency { get; set; } + + [JsonPropertyName("stims")] + public GenerationData Stims { get; set; } + + [JsonPropertyName("backpackLoot")] + public GenerationData BackpackLoot { get; set; } + + [JsonPropertyName("pocketLoot")] + public GenerationData PocketLoot { get; set; } + + [JsonPropertyName("vestLoot")] + public GenerationData VestLoot { get; set; } + + [JsonPropertyName("magazines")] + public GenerationData Magazines { get; set; } + + [JsonPropertyName("specialItems")] + public GenerationData SpecialItems { get; set; } + + [JsonPropertyName("looseLoot")] + public GenerationData LooseLoot { get; set; } +} + public class BotTypeHealth { public List? BodyParts { get; set; } diff --git a/Core/Models/Eft/Common/Tables/GlobalTablesUsings.cs b/Core/Models/Eft/Common/Tables/GlobalTablesUsings.cs index 64968b95..f45cd969 100644 --- a/Core/Models/Eft/Common/Tables/GlobalTablesUsings.cs +++ b/Core/Models/Eft/Common/Tables/GlobalTablesUsings.cs @@ -1,2 +1,2 @@ global using GlobalAmmo = System.Collections.Generic.Dictionary>; -global using GlobalMods = System.Collections.Generic.Dictionary>; \ No newline at end of file +global using GlobalMods = System.Collections.Generic.Dictionary>>; diff --git a/Core/Models/Spt/Bots/BotLootCache.cs b/Core/Models/Spt/Bots/BotLootCache.cs index fa7cdfa7..758f573e 100644 --- a/Core/Models/Spt/Bots/BotLootCache.cs +++ b/Core/Models/Spt/Bots/BotLootCache.cs @@ -5,46 +5,46 @@ namespace Core.Models.Spt.Bots; public class BotLootCache { [JsonPropertyName("backpackLoot")] - public Dictionary? BackpackLoot { get; set; } + public Dictionary? BackpackLoot { get; set; } [JsonPropertyName("pocketLoot")] - public Dictionary? PocketLoot { get; set; } + public Dictionary? PocketLoot { get; set; } [JsonPropertyName("vestLoot")] - public Dictionary? VestLoot { get; set; } + public Dictionary? VestLoot { get; set; } [JsonPropertyName("secureLoot")] - public Dictionary? SecureLoot { get; set; } + public Dictionary? SecureLoot { get; set; } [JsonPropertyName("combinedPoolLoot")] - public Dictionary? CombinedPoolLoot { get; set; } + public Dictionary? CombinedPoolLoot { get; set; } [JsonPropertyName("specialItems")] - public Dictionary? SpecialItems { get; set; } + public Dictionary? SpecialItems { get; set; } [JsonPropertyName("healingItems")] - public Dictionary? HealingItems { get; set; } + public Dictionary? HealingItems { get; set; } [JsonPropertyName("drugItems")] - public Dictionary? DrugItems { get; set; } + public Dictionary? DrugItems { get; set; } [JsonPropertyName("foodItems")] - public Dictionary? FoodItems { get; set; } + public Dictionary? FoodItems { get; set; } [JsonPropertyName("drinkItems")] - public Dictionary? DrinkItems { get; set; } + public Dictionary? DrinkItems { get; set; } [JsonPropertyName("currencyItems")] - public Dictionary? CurrencyItems { get; set; } + public Dictionary? CurrencyItems { get; set; } [JsonPropertyName("stimItems")] - public Dictionary? StimItems { get; set; } + public Dictionary? StimItems { get; set; } [JsonPropertyName("grenadeItems")] - public Dictionary? GrenadeItems { get; set; } + public Dictionary? GrenadeItems { get; set; } } -public static class LootCacheType +public class LootCacheType { public const string Special = "Special"; public const string Backpack = "Backpack"; @@ -59,4 +59,4 @@ public static class LootCacheType public const string FoodItems = "FoodItems"; public const string DrinkItems = "DrinkItems"; public const string CurrencyItems = "CurrencyItems"; -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/BotConfig.cs b/Core/Models/Spt/Config/BotConfig.cs index b9c15c59..a034d7c6 100644 --- a/Core/Models/Spt/Config/BotConfig.cs +++ b/Core/Models/Spt/Config/BotConfig.cs @@ -106,7 +106,7 @@ public class AssaultToBossConversion public bool BossConvertEnabled { get; set; } [JsonPropertyName("bossesToConvertToWeights")] - public Dictionary BossesToConvertToWeights { get; set; } + public Dictionary BossesToConvertToWeights { get; set; } [JsonPropertyName("bossConvertMinMax")] public Dictionary BossConvertMinMax { get; set; } diff --git a/Core/Models/Spt/Config/PmcConfig.cs b/Core/Models/Spt/Config/PmcConfig.cs index a3be7e67..8ed2fd9e 100644 --- a/Core/Models/Spt/Config/PmcConfig.cs +++ b/Core/Models/Spt/Config/PmcConfig.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using Core.Models.Common; using Core.Models.Eft.Common; using Core.Models.Enums; @@ -12,11 +12,11 @@ public class PmcConfig : BaseConfig /** What game version should the PMC have */ [JsonPropertyName("gameVersionWeight")] - public Dictionary GameVersionWeight { get; set; } + public Dictionary GameVersionWeight { get; set; } /** What account type should the PMC have */ [JsonPropertyName("accountTypeWeight")] - public Dictionary AccountTypeWeight { get; set; } + public Dictionary AccountTypeWeight { get; set; } /** Global whitelist/blacklist of vest loot for PMCs */ [JsonPropertyName("vestLoot")] @@ -79,10 +79,10 @@ public class PmcConfig : BaseConfig public List MaxBackpackLootTotalRub { get; set; } [JsonPropertyName("maxPocketLootTotalRub")] - public double MaxPocketLootTotalRub { get; set; } + public int MaxPocketLootTotalRub { get; set; } [JsonPropertyName("maxVestLootTotalRub")] - public double MaxVestLootTotalRub { get; set; } + public int MaxVestLootTotalRub { get; set; } /** Percentage chance a bot from a wave is converted into a PMC, first key = map, second key = bot wildspawn type (assault/exusec), value: min+max chance to be converted */ [JsonPropertyName("convertIntoPmcChance")] @@ -117,7 +117,7 @@ public class PmcConfig : BaseConfig public int? AddPrefixToSameNamePMCAsPlayerChance { get; set; } [JsonPropertyName("lootItemLimitsRub")] - public List? LootItemLimitsRub { get; set; } + public List? LootItemLimitsRub { get; set; } } public class HostilitySettings @@ -171,13 +171,16 @@ public class MinMaxLootValue : MinMax { [JsonPropertyName("value")] public double Value { get; set; } - +} + +public class MinMaxLootItemValue : MinMax +{ [JsonPropertyName("backpack")] public MinMax Backpack { get; set; } - + [JsonPropertyName("pocket")] public MinMax Pocket { get; set; } - + [JsonPropertyName("vest")] public MinMax Vest { get; set; } } diff --git a/Core/Models/Spt/Config/WeatherConfig.cs b/Core/Models/Spt/Config/WeatherConfig.cs index f64fe8ab..f5190889 100644 --- a/Core/Models/Spt/Config/WeatherConfig.cs +++ b/Core/Models/Spt/Config/WeatherConfig.cs @@ -8,16 +8,16 @@ namespace Core.Models.Spt.Config; public class WeatherConfig : BaseConfig { [JsonPropertyName("kind")] - public string Kind { get; set; } = "spt-weather"; + public string? Kind { get; set; } = "spt-weather"; [JsonPropertyName("acceleration")] - public double Acceleration { get; set; } + public double? Acceleration { get; set; } [JsonPropertyName("weather")] - public WeatherValues Weather { get; set; } + public WeatherValues? Weather { get; set; } [JsonPropertyName("seasonDates")] - public List SeasonDates { get; set; } + public List? SeasonDates { get; set; } [JsonPropertyName("overrideSeason")] public Season? OverrideSeason { get; set; } @@ -26,86 +26,86 @@ public class WeatherConfig : BaseConfig public class SeasonDateTimes { [JsonPropertyName("seasonType")] - public Season SeasonType { get; set; } + public Season? SeasonType { get; set; } [JsonPropertyName("name")] - public string Name { get; set; } + public string? Name { get; set; } [JsonPropertyName("startDay")] [JsonConverter(typeof(StringToNumberFactoryConverter))] - public int StartDay { get; set; } + public int? StartDay { get; set; } [JsonPropertyName("startMonth")] [JsonConverter(typeof(StringToNumberFactoryConverter))] - public int StartMonth { get; set; } + public int? StartMonth { get; set; } [JsonPropertyName("endDay")] [JsonConverter(typeof(StringToNumberFactoryConverter))] - public int EndDay { get; set; } + public int? EndDay { get; set; } [JsonPropertyName("endMonth")] [JsonConverter(typeof(StringToNumberFactoryConverter))] - public int EndMonth { get; set; } + public int? EndMonth { get; set; } } public class WeatherValues { [JsonPropertyName("seasonValues")] - public Dictionary SeasonValues { get; set; } + public Dictionary? SeasonValues { get; set; } /** How many hours to generate weather data into the future */ [JsonPropertyName("generateWeatherAmountHours")] - public int GenerateWeatherAmountHours { get; set; } + public int? GenerateWeatherAmountHours { get; set; } /** Length of each weather period */ [JsonPropertyName("timePeriod")] - public WeatherSettings TimePeriod { get; set; } + public WeatherSettings? TimePeriod { get; set; } } public class SeasonalValues { [JsonPropertyName("clouds")] - public WeatherSettings Clouds { get; set; } + public WeatherSettings? Clouds { get; set; } [JsonPropertyName("windSpeed")] - public WeatherSettings WindSpeed { get; set; } + public WeatherSettings? WindSpeed { get; set; } [JsonPropertyName("windDirection")] - public WeatherSettings WindDirection { get; set; } + public WeatherSettings? WindDirection { get; set; } [JsonPropertyName("windGustiness")] - public MinMax WindGustiness { get; set; } + public MinMax? WindGustiness { get; set; } [JsonPropertyName("rain")] - public WeatherSettings Rain { get; set; } + public WeatherSettings? Rain { get; set; } [JsonPropertyName("rainIntensity")] - public MinMax RainIntensity { get; set; } + public MinMax? RainIntensity { get; set; } [JsonPropertyName("fog")] - public WeatherSettings Fog { get; set; } + public WeatherSettings? Fog { get; set; } [JsonPropertyName("temp")] - public TempDayNight Temp { get; set; } + public TempDayNight? Temp { get; set; } [JsonPropertyName("pressure")] - public MinMax Pressure { get; set; } + public MinMax? Pressure { get; set; } } public class TempDayNight { [JsonPropertyName("day")] - public MinMax Day { get; set; } + public MinMax? Day { get; set; } [JsonPropertyName("night")] - public MinMax Night { get; set; } + public MinMax? Night { get; set; } } public class WeatherSettings { [JsonPropertyName("values")] - public List Values { get; set; } + public List? Values { get; set; } [JsonPropertyName("weights")] - public List Weights { get; set; } + public List? Weights { get; set; } } diff --git a/Core/Services/BotLootCacheService.cs b/Core/Services/BotLootCacheService.cs index 1abb9fe1..fc638975 100644 --- a/Core/Services/BotLootCacheService.cs +++ b/Core/Services/BotLootCacheService.cs @@ -1,7 +1,9 @@ using Core.Annotations; using Core.Generators; using Core.Helpers; +using Core.Models.Common; using Core.Models.Eft.Common.Tables; +using Core.Models.Enums; using Core.Models.Spt.Bots; using Core.Models.Utils; using Core.Utils.Cloners; @@ -55,7 +57,8 @@ public class BotLootCacheService string botRole, bool isPmc, string lootType, - BotType botJsonTemplate) + BotType botJsonTemplate, + MinMax? itemPriceMinMax = null) { throw new NotImplementedException(); } @@ -68,7 +71,294 @@ public class BotLootCacheService /// db template for bot having its loot generated protected void AddLootToCache(string botRole, bool isPmc, BotType botJsonTemplate) { - throw new NotImplementedException(); + // Full pool of loot we use to create the various sub-categories with + var lootPool = botJsonTemplate.BotInventory.Items; + + // Flatten all individual slot loot pools into one big pool, while filtering out potentially missing templates + Dictionary specialLootPool = new(); + Dictionary backpackLootPool= new(); + Dictionary pocketLootPool = new(); + Dictionary vestLootPool = new(); + Dictionary secureLootTPool = new(); + Dictionary combinedLootPool = new(); + + if (isPmc) + { + // Replace lootPool from bot json with our own generated list for PMCs + lootPool.Backpack = _cloner.Clone(_pmcLootGenerator.GeneratePMCBackpackLootPool(botRole)); + lootPool.Pockets = _cloner.Clone(_pmcLootGenerator.GeneratePMCPocketLootPool(botRole)); + lootPool.TacticalVest = _cloner.Clone(_pmcLootGenerator.GeneratePMCVestLootPool(botRole)); + } + + // Backpack/Pockets etc + var poolsToProcess = + new Dictionary> + { + { "Backpack", lootPool.Backpack }, + { "Pockets", lootPool.Pockets }, + { "SecuredContainer", lootPool.SecuredContainer }, + { "SpecialLoot", lootPool.SpecialLoot }, + { "TacticalVest", lootPool.TacticalVest } + }; + + + foreach (var kvp in poolsToProcess) + { + // No items to add, skip + if (kvp.Value.Count == 0) + { + continue; + } + + // Sort loot pool into separate buckets + switch (kvp.Key) + { + case "specialloot": + AddItemsToPool(specialLootPool, kvp.Value); + break; + case "pockets": + AddItemsToPool(pocketLootPool, kvp.Value); + break; + case "tacticalvest": + AddItemsToPool(vestLootPool, kvp.Value); + break; + case "securedcontainer": + AddItemsToPool(secureLootTPool, kvp.Value); + break; + case "backpack": + AddItemsToPool(backpackLootPool, kvp.Value); + break; + default: + _logger.Warning($"How did you get here {kvp.Key}"); + break; + } + + // Add all items (if any) to combined pool (excluding secure) + if (kvp.Value.Count > 0 && kvp.Key.ToLower() != "securedcontainer") + { + AddItemsToPool(combinedLootPool, kvp.Value); + } + } + + // Assign whitelisted special items to bot if any exist + var specialLootItems = + botJsonTemplate.BotGeneration.Items.SpecialItems.Whitelist.Count > 0 + ? botJsonTemplate.BotGeneration.Items.SpecialItems.Whitelist + : new Dictionary(); + + // no whitelist, find and assign from combined item pool + if (!specialLootItems.Any()) + { + // key = tpl, value = weight + foreach (var itemKvP in specialLootPool) { + var itemTemplate = _itemHelper.GetItem(itemKvP.Key).Value; + if (!(IsBulletOrGrenade(itemTemplate.Properties) || IsMagazine(itemTemplate.Properties))) + { + specialLootItems[itemKvP.Key] = itemKvP.Value; + } + } + } + + // Assign whitelisted healing items to bot if any exist + var healingItems = + botJsonTemplate.BotGeneration.Items.Healing.Whitelist.Count > 0 + ? botJsonTemplate.BotGeneration.Items.Healing.Whitelist + : new Dictionary(); + + // No whitelist, find and assign from combined item pool + if (!healingItems.Any()) + { + // key = tpl, value = weight + foreach (var itemKvP in combinedLootPool) { + var itemTemplate = _itemHelper.GetItem(itemKvP.Key).Value; + if ( + IsMedicalItem(itemTemplate.Properties) && + itemTemplate.Parent != BaseClasses.STIMULATOR && + itemTemplate.Parent != BaseClasses.DRUGS + ) + { + healingItems[itemKvP.Key] = itemKvP.Value; + } + } + } + + // Assign whitelisted drugs to bot if any exist + var drugItems = botJsonTemplate.BotGeneration.Items.Drugs.Whitelist ?? new Dictionary(); + // no drugs whitelist, find and assign from combined item pool + if (!drugItems.Any()) + { + foreach (var itemKvP in (combinedLootPool)) { + var itemTemplate = _itemHelper.GetItem(itemKvP.Key).Value; + if (IsMedicalItem(itemTemplate.Properties) && itemTemplate.Parent == BaseClasses.DRUGS) + { + drugItems[itemKvP.Key] = itemKvP.Value; + } + } + } + + // Assign whitelisted food to bot if any exist + var foodItems = botJsonTemplate.BotGeneration.Items.Food.Whitelist ?? new Dictionary(); + // No food whitelist, find and assign from combined item pool + if (!foodItems.Any()) + { + foreach (var itemKvP in (combinedLootPool)) { + var itemTemplate = _itemHelper.GetItem(itemKvP.Key).Value; + if (_itemHelper.IsOfBaseclass(itemTemplate.Id, BaseClasses.FOOD)) + { + foodItems[itemKvP.Key] = itemKvP.Value; + } + } + } + + // Assign whitelisted drink to bot if any exist + var drinkItems = botJsonTemplate.BotGeneration.Items.Food.Whitelist ?? new Dictionary(); + // No drink whitelist, find and assign from combined item pool + if (!drinkItems.Any()) + { + foreach (var itemKvP in combinedLootPool) { + var itemTemplate = _itemHelper.GetItem(itemKvP.Key).Value; + if (_itemHelper.IsOfBaseclass(itemTemplate.Id, BaseClasses.DRINK)) + { + drinkItems[itemKvP.Key] = itemKvP.Value; + } + } + } + + // Assign whitelisted currency to bot if any exist + var currencyItems = botJsonTemplate.BotGeneration.Items.Currency.Whitelist ?? new Dictionary(); + // No currency whitelist, find and assign from combined item pool + if (!currencyItems.Any()) + { + foreach (var itemKvP in combinedLootPool) { + var itemTemplate = _itemHelper.GetItem(itemKvP.Key).Value; + if (_itemHelper.IsOfBaseclass(itemTemplate.Id, BaseClasses.MONEY)) + { + currencyItems[itemKvP.Key] = itemKvP.Value; + } + } + } + + // Assign whitelisted stims to bot if any exist + var stimItems = botJsonTemplate.BotGeneration.Items.Stims.Whitelist ?? new Dictionary(); + // No whitelist, find and assign from combined item pool + if (!stimItems.Any()) + { + foreach (var itemKvP in combinedLootPool) { + var itemTemplate = _itemHelper.GetItem(itemKvP.Key).Value; + if (IsMedicalItem(itemTemplate.Properties) && itemTemplate.Parent == BaseClasses.STIMULATOR) + { + stimItems[itemKvP.Key] = itemKvP.Value; + } + } + } + + // Assign whitelisted grenades to bot if any exist + var grenadeItems = botJsonTemplate.BotGeneration.Items.Grenades.Whitelist ?? new Dictionary(); + // no whitelist, find and assign from combined item pool + if (!grenadeItems.Any()) + { + foreach (var itemKvP in combinedLootPool) { + var itemTemplate = _itemHelper.GetItem(itemKvP.Key).Value; + if (IsGrenade(itemTemplate.Properties)) + { + grenadeItems[itemKvP.Key] = itemKvP.Value; + } + } + } + + // Get backpack loot (excluding magazines, bullets, grenades, drink, food and healing/stim items) + var filteredBackpackItems = new Dictionary(); + foreach (var itemKvP in backpackLootPool) { + var itemResult = _itemHelper.GetItem(itemKvP.Key); + if (itemResult.Value is null) + { + continue; + } + var itemTemplate = itemResult.Value; + if ( + IsBulletOrGrenade(itemTemplate.Properties) || + IsMagazine(itemTemplate.Properties) || + IsMedicalItem(itemTemplate.Properties) || + IsGrenade(itemTemplate.Properties) || + IsFood(itemTemplate.Id) || + IsDrink(itemTemplate.Id) || + IsCurrency(itemTemplate.Id) + ) + { + // Is type we don't want as backpack loot, skip + continue; + } + + filteredBackpackItems[itemKvP.Key] = itemKvP.Value; + } + + // Get pocket loot (excluding magazines, bullets, grenades, drink, food medical and healing/stim items) + var filteredPocketItems = new Dictionary(); + foreach (var itemKvP in pocketLootPool) { + var itemResult = _itemHelper.GetItem(itemKvP.Key); + if (itemResult.Value is null) + { + continue; + } + var itemTemplate = itemResult.Value; + if ( + IsBulletOrGrenade(itemTemplate.Properties) || + IsMagazine(itemTemplate.Properties) || + IsMedicalItem(itemTemplate.Properties) || + IsGrenade(itemTemplate.Properties) || + IsFood(itemTemplate.Id) || + IsDrink(itemTemplate.Id) || + IsCurrency(itemTemplate.Id) || + itemTemplate.Properties.Height is null || // lacks height + itemTemplate.Properties.Width is null // lacks width + ) { + continue; + } + + filteredPocketItems[itemKvP.Key] = itemKvP.Value; + } + + // Get vest loot (excluding magazines, bullets, grenades, medical and healing/stim items) + var filteredVestItems = new Dictionary(); + foreach (var itemKvP in vestLootPool) { + var itemResult = _itemHelper.GetItem(itemKvP.Key); + if (itemResult.Value is null) + { + continue; + } + + var itemTemplate = itemResult.Value; + if ( + IsBulletOrGrenade(itemTemplate.Properties) || + IsMagazine(itemTemplate.Properties) || + IsMedicalItem(itemTemplate.Properties) || + IsGrenade(itemTemplate.Properties) || + IsFood(itemTemplate.Id) || + IsDrink(itemTemplate.Id) || + IsCurrency(itemTemplate.Id) + ) + { + continue; + } + + filteredVestItems[itemKvP.Key] = itemKvP.Value; + } + + var cacheForRole = _lootCache[botRole]; + + cacheForRole.HealingItems = healingItems; + cacheForRole.DrugItems = drugItems; + cacheForRole.FoodItems = foodItems; + cacheForRole.DrinkItems = drinkItems; + cacheForRole.CurrencyItems = currencyItems; + cacheForRole.StimItems = stimItems; + cacheForRole.GrenadeItems = grenadeItems; + + cacheForRole.SpecialItems = specialLootItems; + cacheForRole.BackpackLoot = filteredBackpackItems; + cacheForRole.PocketLoot = filteredPocketItems; + cacheForRole.VestLoot = filteredVestItems; + cacheForRole.SecureLoot = secureLootTPool; } /// @@ -81,7 +371,7 @@ public class BotLootCacheService throw new NotImplementedException(); } - protected void AddItemsToPool(Dictionary poolToAddTo, Dictionary poolOfItemsToAdd) + protected void AddItemsToPool(Dictionary poolToAddTo, Dictionary poolOfItemsToAdd) { throw new NotImplementedException(); } diff --git a/Core/Services/ItemBaseClassService.cs b/Core/Services/ItemBaseClassService.cs index d5f52823..7219edd1 100644 --- a/Core/Services/ItemBaseClassService.cs +++ b/Core/Services/ItemBaseClassService.cs @@ -1,18 +1,51 @@ -using Core.Annotations; +using Core.Annotations; using Core.Models.Eft.Common.Tables; +using ILogger = Core.Models.Utils.ILogger; namespace Core.Services; [Injectable(InjectionType.Singleton)] public class ItemBaseClassService { + private readonly ILogger _logger; + private readonly DatabaseService _databaseService; + private readonly LocalisationService _localisationService; + + private bool _cacheGenerated; + private Dictionary> _itemBaseClassesCache; + + public ItemBaseClassService( + ILogger logger, + DatabaseService databaseService, + LocalisationService localisationService) + { + _logger = logger; + _databaseService = databaseService; + _localisationService = localisationService; + } + /** * Create cache and store inside ItemBaseClassService * Store a dict of an items tpl to the base classes it and its parents have */ public void HydrateItemBaseClassCache() { - throw new NotImplementedException(); + // Clear existing cache + _itemBaseClassesCache = new Dictionary>(); + + var items = _databaseService.GetItems(); + var filteredDbItems = (items).Where((x) => x.Value.Type == "Item"); + foreach (var item in filteredDbItems) { + var itemIdToUpdate = item.Value.Id; + if (!_itemBaseClassesCache.ContainsKey(item.Value.Id)) + { + _itemBaseClassesCache[item.Value.Id] = []; + } + + AddBaseItems(itemIdToUpdate, item.Value); + } + + _cacheGenerated = true; } /** @@ -22,7 +55,13 @@ public class ItemBaseClassService */ protected void AddBaseItems(string itemIdToUpdate, TemplateItem item) { - throw new NotImplementedException(); + _itemBaseClassesCache[itemIdToUpdate].Add(item.Parent); + var parent = _databaseService.GetItems()[item.Parent]; + + if (parent.Parent != "") + { + AddBaseItems(itemIdToUpdate, parent); + } } /** @@ -33,7 +72,45 @@ public class ItemBaseClassService */ public bool ItemHasBaseClass(string itemTpl, List baseClasses) { - throw new NotImplementedException(); + if (!_cacheGenerated) + { + HydrateItemBaseClassCache(); + } + + if (itemTpl is null) + { + _logger.Warning("Unable to check itemTpl base class as value passed is null"); + + return false; + } + + // The cache is only generated for item templates with `_type === "Item"`, so return false for any other type, + // including item templates that simply don't exist. + if (!CachedItemIsOfItemType(itemTpl)) + { + return false; + } + + // No item in cache + if (_itemBaseClassesCache.ContainsKey(itemTpl)) + { + return _itemBaseClassesCache[itemTpl].Any(baseClasses.Contains); + } + + _logger.Debug(_localisationService.GetText("baseclass-item_not_found", itemTpl)); + + // Not found in cache, Hydrate again - some mods add items late + HydrateItemBaseClassCache(); + + // Check for item again, return false if item not found a second time + if (_itemBaseClassesCache.ContainsKey(itemTpl)) + { + return _itemBaseClassesCache[itemTpl].Any(baseClasses.Contains); + } + + _logger.Warning(_localisationService.GetText("baseclass-item_not_found_failed", itemTpl)); + + return false; } /** @@ -43,7 +120,7 @@ public class ItemBaseClassService */ private bool CachedItemIsOfItemType(string itemTemplateId) { - throw new NotImplementedException(); + return _databaseService.GetItems()[itemTemplateId]?.Type == "Item"; } /** @@ -53,6 +130,16 @@ public class ItemBaseClassService */ public List GetItemBaseClasses(string itemTpl) { - throw new NotImplementedException(); + if (!_cacheGenerated) + { + HydrateItemBaseClassCache(); + } + + if (!_itemBaseClassesCache.ContainsKey(itemTpl)) + { + return []; + } + + return _itemBaseClassesCache[itemTpl]; } } diff --git a/Core/Services/MailSendService.cs b/Core/Services/MailSendService.cs index bcaf354e..a1656b38 100644 --- a/Core/Services/MailSendService.cs +++ b/Core/Services/MailSendService.cs @@ -450,7 +450,7 @@ public class MailSendService } // Boxes can contain sub-items - if (_itemHelper.IsOfBaseclass(itemTemplate.Id, [BaseClasses.AMMO_BOX])) + if (_itemHelper.IsOfBaseclass(itemTemplate.Id, BaseClasses.AMMO_BOX)) { var boxAndCartridges = new List(); boxAndCartridges.Add(reward); diff --git a/Core/Services/RaidWeatherService.cs b/Core/Services/RaidWeatherService.cs index 7ed12662..41fb4f55 100644 --- a/Core/Services/RaidWeatherService.cs +++ b/Core/Services/RaidWeatherService.cs @@ -54,7 +54,7 @@ public class RaidWeatherService var staringTimestampMs = _timeUtil.GetTodayMidnightTimeStamp(); // How far into future do we generate weather - var futureTimestampToReachMs = staringTimestampMs + _timeUtil.GetHoursAsSeconds(_weatherConfig.Weather.GenerateWeatherAmountHours) * 1000; // Convert to milliseconds + var futureTimestampToReachMs = staringTimestampMs + _timeUtil.GetHoursAsSeconds(_weatherConfig.Weather.GenerateWeatherAmountHours ?? 1) * 1000; // Convert to milliseconds // Keep adding new weather until we have reached desired future date var nextTimestampMs = staringTimestampMs; diff --git a/Core/Services/SeasonalEventService.cs b/Core/Services/SeasonalEventService.cs index dbfc30fe..11badef2 100644 --- a/Core/Services/SeasonalEventService.cs +++ b/Core/Services/SeasonalEventService.cs @@ -320,13 +320,14 @@ public class SeasonalEventService if ( DateIsBetweenTwoDates( currentDate, - seasonRange.StartMonth, - seasonRange.StartDay, - seasonRange.EndMonth, - seasonRange.EndDay) + seasonRange.StartMonth ?? 0, + seasonRange.StartDay ?? 0, + seasonRange.EndMonth ?? 0, + seasonRange.EndDay ?? 0 + ) ) { - return seasonRange.SeasonType; + return seasonRange.SeasonType ?? Season.SUMMER; } } diff --git a/Core/Utils/TimeUtil.cs b/Core/Utils/TimeUtil.cs index 2df541d8..4720e4cf 100644 --- a/Core/Utils/TimeUtil.cs +++ b/Core/Utils/TimeUtil.cs @@ -186,14 +186,15 @@ public class TimeUtil } /// - /// Takes a timestamp and gets difference between Epoch time and time provided resulting in a unixtimestamp (date defaults to utcnow) + /// Takes a date and gets difference between Epoch time and time provided resulting in a timestamp (date defaults to utcnow) + /// This attempts to mimic gettime() in js /// /// /// public long GetTimeStampFromEpoch(DateTime? date = null) { var dateToCompare = date ?? DateTime.UtcNow; - return (long)(dateToCompare - DateTime.UnixEpoch).TotalSeconds; + return (long)(dateToCompare - DateTime.UnixEpoch).TotalMilliseconds; } }