using System.Text.Json.Serialization; using Core.Helpers; using SptCommon.Annotations; using Core.Models.Common; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Spt.Services; using Core.Models.Utils; using Core.Services; using Core.Utils; namespace Core.Generators; [Injectable] public class LootGenerator( ISptLogger _logger, RandomUtil _randomUtil, HashUtil _hashUtil, ItemHelper _itemHelper, PresetHelper _presetHelper, DatabaseService _databaseService, ItemFilterService _itemFilterService ) { /// /// Generate a list of items based on configuration options parameter /// /// parameters to adjust how loot is generated /// An array of loot items public List CreateRandomLoot(LootRequest options) { var result = new List(); var itemTypeCounts = InitItemLimitCounter(options.ItemLimits); // Handle sealed weapon containers var sealedWeaponCrateCount = _randomUtil.GetDouble( options.WeaponCrateCount.Min.Value, options.WeaponCrateCount.Max.Value); if (sealedWeaponCrateCount > 0) { // Get list of all sealed containers from db - they're all the same, just for flavor var itemsDb = _itemHelper.GetItems(); var sealedWeaponContainerPool = (itemsDb).Where((item) => item.Name.Contains("event_container_airdrop")); for (var index = 0; index < sealedWeaponCrateCount; index++) { // Choose one at random + add to results array var chosenSealedContainer = _randomUtil.GetArrayValue(sealedWeaponContainerPool); result.Add( new Item{ Id = _hashUtil.Generate(), Template = chosenSealedContainer.Id, Upd = new Upd{ StackObjectsCount = 1, SpawnedInSession = true }, }); } } // Get items from items.json that have a type of item + not in global blacklist + base type is in whitelist var rewardPoolResults = GetItemRewardPool( options.ItemBlacklist, options.ItemTypeWhitelist, options.UseRewardItemBlacklist.GetValueOrDefault(false), options.AllowBossItems.GetValueOrDefault(false)); // Pool has items we could add as loot, proceed if (rewardPoolResults.ItemPool.Count > 0) { var randomisedItemCount = _randomUtil.GetDouble(options.ItemCount.Min.Value, options.ItemCount.Max.Value); for (var index = 0; index < randomisedItemCount; index++) { if (!FindAndAddRandomItemToLoot(rewardPoolResults.ItemPool, itemTypeCounts, options, result)) { // Failed to add, reduce index so we get another attempt index--; } } } var globalDefaultPresets = _presetHelper.GetDefaultPresets().Values; // Filter default presets to just weapons var randomisedWeaponPresetCount = _randomUtil.GetDouble( options.WeaponPresetCount.Min.Value, options.WeaponPresetCount.Max.Value); if (randomisedWeaponPresetCount > 0) { var weaponDefaultPresets = globalDefaultPresets.Where((preset) => _itemHelper.IsOfBaseclass(preset.Encyclopedia, BaseClasses.WEAPON)).ToList(); if (weaponDefaultPresets.Any()) { for (var index = 0; index < randomisedWeaponPresetCount; index++) { if ( !FindAndAddRandomPresetToLoot( weaponDefaultPresets, itemTypeCounts, rewardPoolResults.Blacklist, result) ) { // Failed to add, reduce index so we get another attempt index--; } } } } // Filter default presets to just armors and then filter again by protection level var randomisedArmorPresetCount = _randomUtil.GetDouble( options.ArmorPresetCount.Min.Value, options.ArmorPresetCount.Max.Value); if (randomisedArmorPresetCount > 0) { var armorDefaultPresets = globalDefaultPresets.Where((preset) => _itemHelper.ArmorItemCanHoldMods(preset.Encyclopedia)); var levelFilteredArmorPresets = armorDefaultPresets.Where((armor) => IsArmorOfDesiredProtectionLevel(armor, options)).ToList(); // Add some armors to rewards if (levelFilteredArmorPresets.Any()) { for (var index = 0; index < randomisedArmorPresetCount; index++) { if ( !FindAndAddRandomPresetToLoot( levelFilteredArmorPresets, itemTypeCounts, rewardPoolResults.Blacklist, result) ) { // Failed to add, reduce index so we get another attempt index--; } } } } return result; } /// /// Generate An array of items /// TODO - handle weapon presets/ammo packs /// /// Dictionary of item tpls with minmax values /// Array of Item public List CreateForcedLoot(Dictionary forcedLootDict) { var result = new List(); var forcedItems = forcedLootDict; foreach (var forcedItemKvP in forcedItems) { var details = forcedLootDict[forcedItemKvP.Key]; var randomisedItemCount = _randomUtil.GetDouble(details.Min.Value, details.Max.Value); // Add forced loot item to result var newLootItem = new Item{ Id = _hashUtil.Generate(), Template = forcedItemKvP.Key, Upd = new Upd{ StackObjectsCount = randomisedItemCount, SpawnedInSession = true, }, }; var splitResults = _itemHelper.SplitStack(newLootItem); result.AddRange(splitResults); } return result; } /// /// Get pool of items from item db that fit passed in param criteria /// /// Prevent these items /// Only allow these items /// Should item.json reward item config be used /// Should boss items be allowed in result /// results of filtering + blacklist used protected ItemRewardPoolResults GetItemRewardPool(List itemTplBlacklist, List itemTypeWhitelist, bool useRewardItemBlacklist, bool allowBossItems) { var itemsDb = _databaseService.GetItems().Values; var itemBlacklist = new HashSet(); itemBlacklist.UnionWith(_itemFilterService.GetBlacklistedItems()); itemBlacklist.UnionWith(itemTplBlacklist); if (useRewardItemBlacklist) { var itemsToAdd = _itemFilterService.GetItemRewardBlacklist(); // Get all items that match the blacklisted types and fold into item blacklist var itemTypeBlacklist = _itemFilterService.GetItemRewardBaseTypeBlacklist(); var itemsMatchingTypeBlacklist = (itemsDb) .Where((templateItem) => _itemHelper.IsOfBaseclasses(templateItem.Parent, itemTypeBlacklist)) .Select((templateItem) => templateItem.Id); // Clear out blacklist itemBlacklist = []; itemBlacklist.UnionWith(itemBlacklist); itemBlacklist.UnionWith(itemsToAdd); itemBlacklist.UnionWith(itemsMatchingTypeBlacklist); } if (!allowBossItems) { foreach (var bossItem in _itemFilterService.GetBossItems()) { itemBlacklist.Add(bossItem); } } var items = itemsDb.Where( (item) => !itemBlacklist.Contains(item.Id) && item.Type.ToLower() == "item" && !item.Properties.QuestItem.GetValueOrDefault(false) && itemTypeWhitelist.Contains(item.Parent)).ToList(); return new ItemRewardPoolResults{ ItemPool = items, Blacklist = itemBlacklist }; } public record ItemRewardPoolResults { public List ItemPool { get; set; } public HashSet Blacklist { get; set; } } /// /// Filter armor items by their front plates protection level - top if it's a helmet /// /// Armor preset to check /// Loot request options - armor level etc /// True if item has desired armor level protected bool IsArmorOfDesiredProtectionLevel(Preset armor, LootRequest options) { string[] relevantSlots = ["front_plate", "helmet_top", "soft_armor_front"]; foreach (var slotId in relevantSlots) { var armorItem = armor.Items.FirstOrDefault((item) => item?.SlotId?.ToLower() == slotId); if (armorItem is null) { continue; } var armorDetails = _itemHelper.GetItem(armorItem.Template).Value; var armorClass = armorDetails.Properties.ArmorClass; return options.ArmorLevelWhitelist.Contains((int)armorClass.Value); } return false; } /// /// Construct item limit record to hold max and current item count for each item type /// /// limits as defined in config /// record, key: item tplId, value: current/max item count allowed protected Dictionary InitItemLimitCounter(Dictionary limits) { throw new NotImplementedException(); } /// /// Find a random item in items.json and add to result array /// /// items to choose from /// item limit counts /// item filters /// array to add found item to /// true if item was valid and added to pool protected bool FindAndAddRandomItemToLoot(object items, object itemTypeCounts, LootRequest options, // TODO: items type was [string, ITemplateItem][], itemTypeCounts was Record List result) { throw new NotImplementedException(); } /// /// Get a randomised stack count for an item between its StackMinRandom and StackMaxSize values /// /// item to get stack count of /// loot options /// stack count protected int GetRandomisedStackCount(TemplateItem item, LootRequest options) { throw new NotImplementedException(); } /// /// Find a random item in items.json and add to result list /// /// Presets to choose from /// Item limit counts /// Items to skip /// List to add chosen preset to /// true if preset was valid and added to pool protected bool FindAndAddRandomPresetToLoot(List presetPool, Dictionary itemTypeCounts, HashSet itemBlacklist, List result) { throw new NotImplementedException(); } /// /// Sealed weapon containers have a weapon + associated mods inside them + assortment of other things (food/meds) /// /// sealed weapon container settings /// List of items with children lists public List> GetSealedWeaponCaseLoot(SealedAirdropContainerSettings containerSettings) { throw new NotImplementedException(); } /// /// Get non-weapon mod rewards for a sealed container /// /// Sealed weapon container settings /// Details for the weapon to reward player /// List of item with children lists protected List> GetSealedContainerNonWeaponModRewards(SealedAirdropContainerSettings containerSettings, TemplateItem weaponDetailsDb) { throw new NotImplementedException(); } /// /// Iterate over the container weaponModRewardLimits settings and create a list of weapon mods to reward player /// /// Sealed weapon container settings /// All items that can be attached/inserted into weapon /// The weapon preset given to player as reward /// List of item with children lists protected List> GetSealedContainerWeaponModRewards(SealedAirdropContainerSettings containerSettings, List linkedItemsToWeapon, Preset chosenWeaponPreset) { throw new NotImplementedException(); } /// /// Handle event-related loot containers - currently just the halloween jack-o-lanterns that give food rewards /// /// /// List of item with children lists public List> GetRandomLootContainerLoot(RewardDetails rewardContainerDetails) { throw new NotImplementedException(); } /// /// Pick a reward item based on the reward details data /// /// /// Single tpl protected string PickRewardItem(RewardDetails rewardContainerDetails) { throw new NotImplementedException(); } } public class ItemLimit { [JsonPropertyName("current")] public double Current { get; set; } [JsonPropertyName("max")] public double Max { get; set; } }