using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Generators; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Eft.Location; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Spt.Services; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Utils; using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Services; [Injectable] public class AirdropService( ConfigServer configServer, ISptLogger _logger, LootGenerator _lootGenerator, HashUtil _hashUtil, WeightedRandomHelper _weightedRandomHelper, ContainerHelper _containerHelper, ServerLocalisationService _serverLocalisationService, ItemFilterService _itemFilterService, ItemHelper _itemHelper ) { protected readonly AirdropConfig _airdropConfig = configServer.GetConfig(); public GetAirdropLootResponse GenerateCustomAirdropLoot(GetAirdropLootRequest request) { if ( _airdropConfig.CustomAirdropMapping.TryGetValue( request.ContainerId, out var customAirdropInformation ) ) { // Found container id, generate specific loot return GenerateAirdropLoot(customAirdropInformation); } _logger.Warning( _serverLocalisationService.GetText( "airdrop-unable_to_find_container_id_generating_random", request.ContainerId ) ); return GenerateAirdropLoot(); } /// /// Handle client/location/getAirdropLoot /// Get loot for an airdrop container /// Generates it randomly based on config/airdrop.json values /// /// OPTIONAL - Desired airdrop type, randomised when not provided /// List of LootItem objects public GetAirdropLootResponse GenerateAirdropLoot(SptAirdropTypeEnum? forcedAirdropType = null) { var airdropType = forcedAirdropType ?? ChooseAirdropType(); if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug($"Chose: {airdropType} for airdrop loot"); } // Common/weapon/etc var airdropConfig = GetAirdropLootConfigByType(airdropType); // generate loot to put into airdrop crate var crateLootPool = airdropConfig.UseForcedLoot.GetValueOrDefault(false) ? _lootGenerator.CreateForcedLoot(airdropConfig.ForcedLoot) : _lootGenerator.CreateRandomLoot(airdropConfig); // Create airdrop crate and add to result in first spot var airdropCrateItem = GetAirdropCrateItem(airdropType); // Filter loot pool to just items that fit crate var crateLoot = GetLootThatFitsContainer(airdropCrateItem, crateLootPool); // Flatten loot into single array ready to be returned var flattenedCrateLoot = crateLoot.SelectMany(x => x).ToList(); // Add crate to front of loot rewards flattenedCrateLoot.Insert(0, airdropCrateItem); // Re-parent loot items to crate we just added foreach (var item in flattenedCrateLoot) { if (item.Id == airdropCrateItem.Id) // Crate itself, skip { continue; } // no parentId = root item, update item to have crate as parent if (string.IsNullOrEmpty(item.ParentId)) { item.ParentId = airdropCrateItem.Id; item.SlotId = "main"; } } return new GetAirdropLootResponse { Icon = airdropConfig.Icon, Container = flattenedCrateLoot, }; } /// /// Check if the items provided fit into the passed in container /// /// Crate item to fit items into /// Item pool to try and fit into container /// Items that will fit container protected List> GetLootThatFitsContainer( Item container, List> crateLootPool ) { // list of root item + children in list var lootResult = new List>(); // Get 2d mapping of container var containerMap = _itemHelper.GetContainerMapping(container.Template); var failedToFitAttemptCount = 0; foreach (var itemAndChildren in crateLootPool) { // Get x/y size of item (weapons get larger with children attached) var itemSize = _itemHelper.GetItemSize(itemAndChildren, itemAndChildren[0].Id); // Look for open slot to put chosen item into var result = _containerHelper.FindSlotForItem( containerMap, itemSize.Width, itemSize.Height ); if (result.Success.GetValueOrDefault(false)) { // It Fits, add item + children lootResult.AddRange(itemAndChildren); // Update container with item we just added _containerHelper.FillContainerMapWithItem( containerMap, result.X.Value, result.Y.Value, itemSize.Width, itemSize.Height, result.Rotation.GetValueOrDefault(false) ); continue; } if (failedToFitAttemptCount > 3) // 3 attempts to fit an item, container is probably full, stop trying to add more { _logger.Debug( $"Airdrop is too full of loot to add: {itemAndChildren[0].Template} after {failedToFitAttemptCount} attempts, stopped adding more" ); break; } // Can't fit item, skip failedToFitAttemptCount++; } return lootResult; } /// /// Create a container create item based on passed in airdrop type /// /// What type of container: weapon/common etc /// Item protected Item GetAirdropCrateItem(SptAirdropTypeEnum airdropType) { var airdropContainer = new Item { Id = new MongoId(), Template = string.Empty, // Chosen below later Upd = new Upd { SpawnedInSession = true, StackObjectsCount = 1 }, }; switch (airdropType) { case SptAirdropTypeEnum.foodMedical: airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_MEDICAL_CRATE; break; case SptAirdropTypeEnum.barter: airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_SUPPLY_CRATE; break; case SptAirdropTypeEnum.weaponArmor: airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_WEAPON_CRATE; break; case SptAirdropTypeEnum.mixed: airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE; break; case SptAirdropTypeEnum.radar: airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_TECHNICAL_SUPPLY_CRATE_EVENT_1; break; default: airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE; break; } return airdropContainer; } /// /// Randomly pick a type of airdrop loot using weighted values from config /// /// airdrop type value protected SptAirdropTypeEnum ChooseAirdropType() { var possibleAirdropTypes = _airdropConfig.AirdropTypeWeightings; return _weightedRandomHelper.GetWeightedValue(possibleAirdropTypes); } /// /// Get the configuration for a specific type of airdrop /// /// Type of airdrop to get settings for /// LootRequest protected AirdropLootRequest GetAirdropLootConfigByType(SptAirdropTypeEnum? airdropType) { if (!_airdropConfig.Loot.TryGetValue(airdropType.ToString(), out var lootSettingsByType)) { _logger.Error( _serverLocalisationService.GetText( "location-unable_to_find_airdrop_drop_config_of_type", airdropType ) ); // TODO: Get Radar airdrop to work. Atm Radar will default to common supply drop (mixed) // Default to common lootSettingsByType = _airdropConfig.Loot[nameof(AirdropTypeEnum.Common)]; } // Get all items that match the blacklisted types and fold into item blacklist var itemTypeBlacklist = _itemFilterService.GetItemRewardBaseTypeBlacklist(); var itemsMatchingTypeBlacklist = _itemHelper .GetItems() .Where(templateItem => !string.IsNullOrEmpty(templateItem.Parent)) .Where(templateItem => _itemHelper.IsOfBaseclasses(templateItem.Parent, itemTypeBlacklist) ) .Select(templateItem => templateItem.Id) .ToHashSet(); var itemBlacklist = new HashSet(); itemBlacklist.UnionWith(lootSettingsByType.ItemBlacklist); itemBlacklist.UnionWith(_itemFilterService.GetItemRewardBlacklist()); itemBlacklist.UnionWith(_itemFilterService.GetBossItems()); itemBlacklist.UnionWith(itemsMatchingTypeBlacklist); return new AirdropLootRequest { Icon = lootSettingsByType.Icon, WeaponPresetCount = lootSettingsByType.WeaponPresetCount, ArmorPresetCount = lootSettingsByType.ArmorPresetCount, ItemCount = lootSettingsByType.ItemCount, WeaponCrateCount = lootSettingsByType.WeaponCrateCount, ItemBlacklist = itemBlacklist, ItemTypeWhitelist = lootSettingsByType.ItemTypeWhitelist, ItemLimits = lootSettingsByType.ItemLimits, ItemStackLimits = lootSettingsByType.ItemStackLimits, ArmorLevelWhitelist = lootSettingsByType.ArmorLevelWhitelist, AllowBossItems = lootSettingsByType.AllowBossItems, UseForcedLoot = lootSettingsByType.UseForcedLoot, ForcedLoot = lootSettingsByType.ForcedLoot, UseRewardItemBlacklist = lootSettingsByType.UseRewardItemBlacklist, BlockSeasonalItemsOutOfSeason = lootSettingsByType.BlockSeasonalItemsOutOfSeason, }; } }