using System.Collections.Frozen; using SPTarkov.Common.Extensions; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils.Cloners; namespace SPTarkov.Server.Core.Helpers; [Injectable] public class InRaidHelper( InventoryHelper inventoryHelper, ConfigServer configServer, ICloner cloner, DatabaseService databaseService ) { protected static readonly FrozenSet _pocketSlots = [ "pocket1", "pocket2", "pocket3", "pocket4", ]; protected readonly InRaidConfig _inRaidConfig = configServer.GetConfig(); protected readonly LostOnDeathConfig _lostOnDeathConfig = configServer.GetConfig(); /// /// Deprecated. Reset the skill points earned in a raid to 0, ready for next raid. /// /// Profile to update protected void ResetSkillPointsEarnedDuringRaid(PmcData profile) { foreach (var skill in profile.Skills.Common) { skill.PointsEarnedDuringSession = 0.0; } } /// /// Update a player's inventory post-raid. /// Remove equipped items from pre-raid. /// Add new items found in raid to profile. /// Store insurance items in profile. /// /// Session id /// Profile to update /// Profile returned by client after a raid /// Indicates if the player survived the raid /// Indicates if it is a transfer operation public void SetInventory( MongoId sessionID, PmcData serverProfile, PmcData postRaidProfile, bool isSurvived, bool isTransfer ) { // Store insurance (as removeItem() removes insured items) var insured = cloner.Clone(serverProfile.InsuredItems); // Remove equipment and loot items stored on player from server profile in preparation for data from client being added inventoryHelper.RemoveItem( serverProfile, serverProfile.Inventory.Equipment.Value, sessionID ); // Remove quest items stored on player from server profile in preparation for data from client being added inventoryHelper.RemoveItem( serverProfile, serverProfile.Inventory.QuestRaidItems.Value, sessionID ); // Get all items that have a parent of `serverProfile.Inventory.equipment` (All items player had on them at end of raid) var postRaidInventoryItems = postRaidProfile.Inventory.Items.FindAndReturnChildrenAsItems( postRaidProfile.Inventory.Equipment.Value ); // Get all items that have a parent of `serverProfile.Inventory.questRaidItems` (Quest items player had on them at end of raid) var postRaidQuestItems = postRaidProfile.Inventory.Items.FindAndReturnChildrenAsItems( postRaidProfile.Inventory.QuestRaidItems.Value ); // Handle Removing of FIR status if player did not survive + not transferring // Do after above filtering code to reduce work done if (!isSurvived && !isTransfer && !_inRaidConfig.AlwaysKeepFoundInRaidOnRaidEnd) { RemoveFiRStatusFromItems(postRaidProfile.Inventory.Items); } // Add items from client profile into server profile AddItemsToInventory(postRaidInventoryItems, serverProfile.Inventory.Items); // Add quest items from client profile into server profile AddItemsToInventory(postRaidQuestItems, serverProfile.Inventory.Items); serverProfile.Inventory.FastPanel = postRaidProfile.Inventory.FastPanel; // Quick access items bar serverProfile.InsuredItems = insured; } /// /// Remove FiR status from items. /// /// Items to process protected void RemoveFiRStatusFromItems(List items) { var dbItems = databaseService.GetItems(); var itemsToRemovePropertyFrom = items.Where(item => { // Has upd object + upd.SpawnedInSession property + not a quest item return (item.Upd?.SpawnedInSession ?? false) && !(dbItems[item.Template].Properties.QuestItem ?? false) && !( _inRaidConfig.KeepFiRSecureContainerOnDeath && item.ItemIsInsideContainer("SecuredContainer", items) ); }); foreach (var item in itemsToRemovePropertyFrom) { if (item.Upd is not null) { item.Upd.SpawnedInSession = false; } } } /// /// Add items from one parameter into another. /// /// Items we want to add /// Location to add items to protected void AddItemsToInventory( IEnumerable itemsToAdd, List serverInventoryItems ) { foreach (var itemToAdd in itemsToAdd) { // Try to find index of item to determine if we should add or replace var existingItemIndex = serverInventoryItems.FindIndex(inventoryItem => inventoryItem.Id == itemToAdd.Id ); if (existingItemIndex != -1) { // Replace existing item serverInventoryItems.RemoveAt(existingItemIndex); } // Add new item serverInventoryItems.Add(itemToAdd); } } /// /// Clear PMC inventory of all items except those that are exempt. /// Used post-raid to remove items after death. /// /// Player profile /// Session id public void DeleteInventory(PmcData pmcData, MongoId sessionId) { // Get inventory item ids to remove from players profile var itemIdsToDeleteFromProfile = GetInventoryItemsLostOnDeath(pmcData) .Select(item => item.Id); foreach (var itemIdToDelete in itemIdsToDeleteFromProfile) // Items inside containers are handled as part of function { inventoryHelper.RemoveItem(pmcData, itemIdToDelete, sessionId); } // Remove contents of fast panel pmcData.Inventory.FastPanel = new Dictionary(); } /// /// Remove FiR status from designated container. /// /// Session id /// Player profile /// Container slot id to find items for and remove FiR from public void RemoveFiRStatusFromItemsInContainer( MongoId sessionId, PmcData pmcData, string secureContainerSlotId ) { if (!pmcData.Inventory.Items.Any(item => item.SlotId == secureContainerSlotId)) { return; } List itemsInsideContainer = []; foreach ( var inventoryItem in pmcData.Inventory.Items.Where(item => item.Upd is not null && item.SlotId != "hideout" ) ) { if (inventoryItem.ItemIsInsideContainer(secureContainerSlotId, pmcData.Inventory.Items)) { itemsInsideContainer.Add(inventoryItem); } } foreach ( var item in itemsInsideContainer.Where(item => item.Upd?.SpawnedInSession ?? false) ) { item.Upd.SpawnedInSession = false; } } /// /// Get a list of items from a profile that will be lost on death. /// /// Profile to get items from /// List of items lost on death protected List GetInventoryItemsLostOnDeath(PmcData pmcProfile) { var inventoryItems = pmcProfile.Inventory.Items ?? []; var equipmentRootId = pmcProfile?.Inventory?.Equipment; var questRaidItemContainerId = pmcProfile?.Inventory?.QuestRaidItems; return inventoryItems .Where(item => { // Keep items flagged as kept after death if (IsItemKeptAfterDeath(pmcProfile, item)) { return false; } // Remove normal items or quest raid items if (item.ParentId == equipmentRootId || item.ParentId == questRaidItemContainerId) { return true; } // Pocket items are lost on death // Ensure we don't pick up pocket items from mannequins if ( item.SlotId.StartsWith("pocket") && pmcProfile.DoesItemHaveRootId(item, pmcProfile.Inventory.Equipment) ) { return true; } return false; }) .ToList(); } /// /// Does the provided item's slotId mean it's kept on the player after death? /// /// Player profile /// Item to check should be kept /// true if item is kept after death protected bool IsItemKeptAfterDeath(PmcData pmcData, Item itemToCheck) { // Base inventory items are always kept if (itemToCheck.ParentId is null) { return true; } // Is item equipped on player if (itemToCheck.ParentId == pmcData.Inventory.Equipment) { // Check slot id against config, true = delete, false = keep, undefined = delete var discard = _lostOnDeathConfig.Equipment.GetByJsonProp(itemToCheck.SlotId); if (discard) // Lost on death { return false; } return true; } // Should we keep items in pockets on death if (!_lostOnDeathConfig.Equipment.PocketItems && _pocketSlots.Contains(itemToCheck.SlotId)) { return true; } // Is quest item + quest item not lost on death if ( itemToCheck.ParentId == pmcData.Inventory.QuestRaidItems && !_lostOnDeathConfig.QuestItems ) { return true; } // special slots are always kept after death if ( (itemToCheck.SlotId?.Contains("SpecialSlot") ?? false) && _lostOnDeathConfig.SpecialSlotItems ) { return true; } // All other cases item is lost return false; } }