Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs
T
Eero 7b8275100d Improve thread safety and performance in core systems
Refactors event, entity, and physics management to use thread-safe and lock-free data structures (Immutable collections, ConcurrentQueue, ConcurrentDictionary, Channel) for improved concurrency and performance. Replaces O(n) queue lookups with O(1) set/dictionary checks, ensures atomic updates for shared state, and optimizes queue draining and deferred action processing. Updates related code to use new APIs and patterns, and adds documentation for thread safety and workflow.
2025-12-29 16:47:10 +08:00

297 lines
15 KiB
C#

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<Client, List<ushort>[]> receivedItemIds = new Dictionary<Client, List<ushort>[]>();
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<ushort>[] receivedItemIdsFromClient))
{
receivedItemIdsFromClient = new List<ushort>[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<Item> prevItems = new List<Item>(AllItems.Distinct());
List<Inventory> prevItemInventories = new List<Inventory>() { 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<Item, bool> GetItemAccessibility()
{
Dictionary<Item, bool> itemAccessibility = new Dictionary<Item, bool>();
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(null);
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<Pickable>() 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<Holdable>();
if (holdable != null && !holdable.CanBeDeattached()) { continue; }
bool itemAccessDenied = !prevItems.Contains(item) && !itemAccessibility[item] &&
(sender.Character == null || item.PreviousParentInventory == null || !sender.Character.CanAccessInventory(item.PreviousParentInventory));
//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.TryEnqueuePositionUpdate(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;
}
}
}