using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; namespace Barotrauma { partial class Inventory : IClientSerializable { private readonly Dictionary[]> receivedItemIds = new Dictionary[]>(); public void ServerEventRead(IReadMessage msg, Client sender) { // if the dictionary doesn't contain the client entry, create a new one if (!receivedItemIds.TryGetValue(sender, out List[] receivedItemIdsFromClient)) { receivedItemIdsFromClient = new List[capacity]; receivedItemIds.Add(sender, receivedItemIdsFromClient); } // Read some item ids from the message. readyToApply waits for all the data from possible multiple messages. SharedRead(msg, receivedItemIdsFromClient, out bool readyToApply); if (!readyToApply) { return; } if (sender == null || sender.Character == null) { return; } if (!IsInventoryAccessible()) { CreateCorrectiveNetworkEvent(); return; } List prevItems = new List(AllItems.Distinct()); List prevItemInventories = new List() { this }; //we need to check which of the items the client (sender) can access at this point, before we start shuffling things around //otherwise if you're e.g. holding an item to access a cabinet, and picking up an item from the cabinet unequips the item you're holding, //you would fail to pick up the item because it gets unequipped before checking whether you can access the cabinet. var itemAccessibility = GetItemAccessibility(); HandleRemovedItems(); HandleAddedItems(); EnsureItemsInBothHands(sender.Character); receivedItemIds.Remove(sender); CreateNetworkEvent(); foreach (Inventory prevInventory in prevItemInventories.Distinct()) { if (prevInventory != this) { prevInventory?.CreateNetworkEvent(); } } ServerLogAddedItems(); ServerLogRemovedItems(); #region local functions bool IsInventoryAccessible() => sender.Character.CanAccessInventory(this, IsDragAndDropGiveAllowed ? CharacterInventory.AccessLevel.AllowFriendly : CharacterInventory.AccessLevel.AllowBotsAndPets); void CreateCorrectiveNetworkEvent() { // create a network event to correct the client's inventory state. // Otherwise they may have an item in their inventory they shouldn't have been able to pick up, // and receiving an event for that inventory later will cause the item to be dropped CreateNetworkEvent(); for (int i = 0; i < capacity; i++) { foreach (ushort itemId in receivedItemIdsFromClient[i]) { if (Entity.FindEntityByID(itemId) is not Item item) { continue; } item.PositionUpdateInterval = 0.0f; if (item.ParentInventory != null && item.ParentInventory != this) { item.ParentInventory.CreateNetworkEvent(); } } } } Dictionary GetItemAccessibility() { Dictionary itemAccessibility = new Dictionary(); for (int i = 0; i < capacity; i++) { // for every item that the new inventory state contains foreach (ushort itemId in receivedItemIdsFromClient[i]) { // if there is no such item, skip if (Entity.FindEntityByID(itemId) is not Item item) { continue; } // add entry: can the sender access the item? itemAccessibility[item] = item.CanClientAccess(sender); } } // we now have accessibility for every item in the new inventory state // but not for the items that were in the inventory before and perhaps dropped, so let's add those as well foreach (var item in prevItems) { if (!itemAccessibility.ContainsKey(item)) { itemAccessibility[item] = item.CanClientAccess(sender); } } return itemAccessibility; } void HandleRemovedItems() { for (int slotIndex = 0; slotIndex < capacity; slotIndex++) { foreach (Item item in slots[slotIndex].Items.ToList()) { bool shouldBeRemoved = !receivedItemIdsFromClient[slotIndex].Contains(item.ID) && item.IsInteractable(sender.Character); // item is interactable to sender: not hidden and player team if (shouldBeRemoved) { bool itemAccessDenied = prevItems.Contains(item) && // if the item was in the inventory before !itemAccessibility[item] && // and the sender is not allowed to access it (item.PreviousParentInventory == null || // and either the item has no previous inventory !sender.Character.CanAccessInventory(item.PreviousParentInventory)); // or the sender can't access the previous inventory if (itemAccessDenied) { #if DEBUG || UNSTABLE DebugConsole.NewMessage($"Client {sender.Name} failed to drop item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow); #endif continue; } Item droppedItem = item; Entity prevOwner = Owner; Inventory previousInventory = droppedItem.ParentInventory; droppedItem.Drop(sender.Character); droppedItem.PreviousParentInventory = previousInventory; var previousCharacterInventory = prevOwner switch { Item itemInventory => itemInventory.FindParentInventory(inventory => inventory is CharacterInventory) as CharacterInventory, Character character => character.Inventory, _ => null }; if (previousCharacterInventory != null && previousCharacterInventory != sender.Character?.Inventory) { GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(previousCharacterInventory, sender, droppedItem); } if (droppedItem.body != null && prevOwner != null) { droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f); } } } foreach (ushort id in receivedItemIdsFromClient[slotIndex]) { Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item; prevItemInventories.Add(newItem?.ParentInventory); } } } void HandleAddedItems() { for (int slotIndex = 0; slotIndex < capacity; slotIndex++) { foreach (ushort id in receivedItemIdsFromClient[slotIndex]) { if (Entity.FindEntityByID(id) is not Item item || slots[slotIndex].Contains(item)) { continue; } if (item.GetComponent() is not Pickable pickable || (pickable.IsAttached && !pickable.PickingDone) || item.AllowedSlots.None() || !item.IsInteractable(sender.Character)) { DebugConsole.AddWarning($"Client {sender.Name} failed to put \"{item}\" in the inventory of {Owner} (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})", item.Prefab.ContentPackage); continue; } if (GameMain.Server != null) { var holdable = item.GetComponent(); if (holdable != null && !holdable.CanBeDeattached()) { continue; } bool itemAccessDenied = !prevItems.Contains(item) && !itemAccessibility[item] && (item.PreviousParentInventory == null || !sender.Character.CanAccessInventory(item.PreviousParentInventory)); // Prevent modified clients from being able to steal items from characters by item swapping with an existing item // due to drag and drop being enabled if (!sender.Character.CanAccessInventory(this, CharacterInventory.AccessLevel.AllowBotsAndPets) && GetItemAt(slotIndex) != null) { itemAccessDenied = true; } //more restricted "adding" of handcuffs: we can't allow putting handcuffs on a player just because dragging and dropping is allowed if (item.HasTag(Tags.HandLockerItem) && !itemAccessDenied) { itemAccessDenied = !sender.Character.CanAccessInventory(this, CharacterInventory.AccessLevel.AllowBotsAndPets); } if (itemAccessDenied) { #if DEBUG || UNSTABLE DebugConsole.NewMessage($"Client {sender.Name} failed to put \"{item}\" in the inventory of {Owner} (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow); #endif if (item.body != null && !sender.PendingPositionUpdates.Contains(item)) { sender.PendingPositionUpdates.Enqueue(item); } item.PositionUpdateInterval = 0.0f; continue; } } TryPutItem(item, slotIndex, true, true, sender.Character, false); for (int j = 0; j < capacity; j++) { if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID)) { slots[j].RemoveItem(item); } } } } } void ServerLogAddedItems() { foreach (Item item in AllItems.DistinctBy(it => it.Prefab)) { if (item == null) { continue; } if (!prevItems.Contains(item)) { int amount = AllItems.Count(it => it.Prefab == item.Prefab && !prevItems.Contains(it)); string amountText = amount > 1 ? $"x{amount} " : string.Empty; if (Owner == sender.Character) { HumanAIController.ItemTaken(item, sender.Character); GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} picked up {amountText}{item.Name}", ServerLog.MessageType.Inventory); } else { GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} placed {amountText}{item.Name} in the inventory of {Owner}", ServerLog.MessageType.Inventory); } } } } void ServerLogRemovedItems() { var droppedItems = prevItems.Where(it => it != null && !AllItems.Contains(it)); foreach (Item item in droppedItems.DistinctBy(it => it.Prefab)) { var matchingItems = prevItems.Where(it => it.Prefab == item.Prefab && !AllItems.Contains(it)); int amount = matchingItems.Count(); string amountText = amount > 1 ? $"x{amount} " : string.Empty; if (Owner == sender.Character) { GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} dropped {amountText}{item.Name}", ServerLog.MessageType.Inventory); } else { GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} removed {amountText}{item.Name} from the inventory of {Owner}", ServerLog.MessageType.Inventory); } item.CreateDroppedStack(matchingItems, allowClientExecute: true); } } #endregion } private void EnsureItemsInBothHands(Character character) { if (this is not CharacterInventory charInv) { return; } int leftHandSlot = charInv.FindLimbSlot(InvSlotType.LeftHand), rightHandSlot = charInv.FindLimbSlot(InvSlotType.RightHand); if (IsSlotIndexOutOfBound(leftHandSlot) || IsSlotIndexOutOfBound(rightHandSlot)) { return; } TryPutInOppositeHandSlot(rightHandSlot, leftHandSlot); TryPutInOppositeHandSlot(leftHandSlot, rightHandSlot); void TryPutInOppositeHandSlot(int originalSlot, int otherHandSlot) { const InvSlotType bothHandSlot = InvSlotType.LeftHand | InvSlotType.RightHand; foreach (Item it in slots[originalSlot].Items) { if (it.AllowedSlots.None(static s => s.HasFlag(bothHandSlot)) || slots[otherHandSlot].Contains(it)) { continue; } TryPutItem(it, otherHandSlot, true, true, character, false); } } bool IsSlotIndexOutOfBound(int index) => index < 0 || index >= slots.Length; } } }