Replaced static lists and dictionaries with thread-safe ConcurrentDictionary or ThreadLocal collections for various item components and systems. Updated all relevant code to use snapshots (ToArray, ToList) for safe iteration, and added helper methods for marking and clearing changed connections. These changes improve thread safety and prevent potential concurrency issues in multi-threaded scenarios.
1188 lines
44 KiB
C#
1188 lines
44 KiB
C#
using Barotrauma.Extensions;
|
|
using Barotrauma.Items.Components;
|
|
using Barotrauma.Networking;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class Inventory
|
|
{
|
|
public const int MaxPossibleStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63
|
|
|
|
public const int MaxItemsPerNetworkEvent = 128;
|
|
|
|
public class ItemSlot
|
|
{
|
|
private readonly List<Item> items = new List<Item>(MaxPossibleStackSize);
|
|
|
|
public bool HideIfEmpty;
|
|
|
|
public IReadOnlyList<Item> Items => items;
|
|
|
|
private readonly Inventory inventory;
|
|
|
|
public ItemSlot(Inventory inventory)
|
|
{
|
|
this.inventory = inventory;
|
|
}
|
|
|
|
public bool CanBePut(Item item, bool ignoreCondition = false)
|
|
{
|
|
if (item == null) { return false; }
|
|
if (items.Count > 0)
|
|
{
|
|
if (!ignoreCondition)
|
|
{
|
|
if (item.IsFullCondition)
|
|
{
|
|
if (items.Any(it => !it.IsFullCondition)) { return false; }
|
|
}
|
|
else if (MathUtils.NearlyEqual(item.Condition, 0.0f))
|
|
{
|
|
if (items.Any(it => !MathUtils.NearlyEqual(it.Condition, 0.0f))) { return false; }
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
if (items[0].Quality != item.Quality) { return false; }
|
|
if (items[0].Prefab.Identifier != item.Prefab.Identifier || items.Count + 1 > item.Prefab.GetMaxStackSize(inventory))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can an instance of the item prefab be put into the slot?
|
|
/// Note that if the condition and quality aren't given, they are ignored, and the method can return true even if the item can't go in the slot
|
|
/// due to it being occupied by another item with a different condition or quality (which disallows stacking).
|
|
/// </summary>
|
|
public bool CanProbablyBePut(ItemPrefab itemPrefab, float? condition = null, int? quality = null)
|
|
{
|
|
if (itemPrefab == null) { return false; }
|
|
if (items.Count > 0)
|
|
{
|
|
if (condition.HasValue)
|
|
{
|
|
if (MathUtils.NearlyEqual(condition.Value, 0.0f))
|
|
{
|
|
if (items.Any(it => it.Condition > 0.0f)) { return false; }
|
|
}
|
|
else if (MathUtils.NearlyEqual(condition.Value, itemPrefab.Health))
|
|
{
|
|
if (items.Any(it => !it.IsFullCondition)) { return false; }
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (items.Any(it => !it.IsFullCondition)) { return false; }
|
|
}
|
|
|
|
if (quality.HasValue)
|
|
{
|
|
if (items[0].Quality != quality.Value) { return false; }
|
|
}
|
|
|
|
if (items[0].Prefab.Identifier != itemPrefab.Identifier ||
|
|
items.Count + 1 > itemPrefab.GetMaxStackSize(inventory))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <param name="maxStackSize">Defaults to <see cref="ItemPrefab.MaxStackSize"/> if null</param>
|
|
public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null, float? condition = null, bool ignoreItemsInSlot = false)
|
|
{
|
|
if (itemPrefab == null) { return 0; }
|
|
maxStackSize ??= itemPrefab.GetMaxStackSize(inventory);
|
|
if (items.Count > 0 && !ignoreItemsInSlot)
|
|
{
|
|
if (condition.HasValue)
|
|
{
|
|
if (MathUtils.NearlyEqual(condition.Value, 0.0f))
|
|
{
|
|
if (items.Any(it => it.Condition > 0.0f)) { return 0; }
|
|
}
|
|
else if (MathUtils.NearlyEqual(condition.Value, itemPrefab.Health))
|
|
{
|
|
if (items.Any(it => !it.IsFullCondition)) { return 0; }
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (items.Any(it => !it.IsFullCondition)) { return 0; }
|
|
}
|
|
if (items[0].Prefab.Identifier != itemPrefab.Identifier) { return 0; }
|
|
return maxStackSize.Value - items.Count;
|
|
}
|
|
else
|
|
{
|
|
return maxStackSize.Value;
|
|
}
|
|
}
|
|
|
|
public void Add(Item item)
|
|
{
|
|
if (item == null)
|
|
{
|
|
throw new InvalidOperationException("Tried to add a null item to an inventory slot.");
|
|
}
|
|
if (items.Count > 0)
|
|
{
|
|
if (items[0].Prefab.Identifier != item.Prefab.Identifier)
|
|
{
|
|
throw new InvalidOperationException("Tried to stack different types of items.");
|
|
}
|
|
else if (items.Count + 1 > item.Prefab.GetMaxStackSize(inventory))
|
|
{
|
|
throw new InvalidOperationException($"Tried to add an item to a full inventory slot (stack already full, x{items.Count} {items.First().Prefab.Identifier}).");
|
|
}
|
|
}
|
|
if (items.Contains(item)) { return; }
|
|
|
|
//keep lowest-condition items at the top of the stack
|
|
int index = 0;
|
|
for (int i = 0; i < items.Count; i++)
|
|
{
|
|
if (items[i].Condition > item.Condition)
|
|
{
|
|
break;
|
|
}
|
|
index++;
|
|
}
|
|
items.Insert(index, item);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes one item from the slot
|
|
/// </summary>
|
|
public Item RemoveItem()
|
|
{
|
|
if (items.Count == 0) { return null; }
|
|
|
|
var item = items[0];
|
|
items.RemoveAt(0);
|
|
return item;
|
|
}
|
|
|
|
public void RemoveItem(Item item)
|
|
{
|
|
items.Remove(item);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all items from the slot
|
|
/// </summary>
|
|
public void RemoveAllItems()
|
|
{
|
|
items.Clear();
|
|
}
|
|
|
|
public void RemoveWhere(Func<Item, bool> predicate)
|
|
{
|
|
items.RemoveAll(it => predicate(it));
|
|
}
|
|
|
|
public bool Any()
|
|
{
|
|
return items.Count > 0;
|
|
}
|
|
|
|
public bool Empty()
|
|
{
|
|
return items.Count == 0;
|
|
}
|
|
|
|
public Item First()
|
|
{
|
|
return items[0];
|
|
}
|
|
|
|
public Item FirstOrDefault()
|
|
{
|
|
return items.FirstOrDefault();
|
|
}
|
|
|
|
public Item LastOrDefault()
|
|
{
|
|
return items.LastOrDefault();
|
|
}
|
|
|
|
public bool Contains(Item item)
|
|
{
|
|
return items.Contains(item);
|
|
}
|
|
|
|
}
|
|
|
|
public readonly Entity Owner;
|
|
|
|
/// <summary>
|
|
/// Capacity, or the number of slots in the inventory.
|
|
/// </summary>
|
|
protected readonly int capacity;
|
|
protected readonly ItemSlot[] slots;
|
|
|
|
public bool Locked;
|
|
|
|
protected float syncItemsDelay;
|
|
|
|
|
|
private int extraStackSize;
|
|
public int ExtraStackSize
|
|
{
|
|
get => extraStackSize;
|
|
set => extraStackSize = MathHelper.Max(value, 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// All items contained in the inventory. Stacked items are returned as individual instances. DO NOT modify the contents of the inventory while enumerating this list.
|
|
/// </summary>
|
|
public virtual IEnumerable<Item> AllItems
|
|
{
|
|
get
|
|
{
|
|
return GetAllItems(checkForDuplicates: this is CharacterInventory);
|
|
}
|
|
}
|
|
|
|
private readonly List<Item> allItemsList = new List<Item>();
|
|
/// <summary>
|
|
/// All items contained in the inventory. Allows modifying the contents of the inventory while being enumerated.
|
|
/// </summary>
|
|
public IEnumerable<Item> AllItemsMod
|
|
{
|
|
get
|
|
{
|
|
allItemsList.Clear();
|
|
allItemsList.AddRange(AllItems);
|
|
return allItemsList;
|
|
}
|
|
}
|
|
|
|
public int Capacity
|
|
{
|
|
get { return capacity; }
|
|
}
|
|
|
|
public static bool IsDragAndDropGiveAllowed
|
|
{
|
|
get
|
|
{
|
|
// allowed for single player
|
|
if (GameMain.NetworkMember == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// controlled by server setting in multiplayer
|
|
return GameMain.NetworkMember.ServerSettings.AllowDragAndDropGive;
|
|
}
|
|
}
|
|
|
|
public int EmptySlotCount => slots.Count(i => !i.Empty());
|
|
|
|
public bool AllowSwappingContainedItems = true;
|
|
|
|
public Inventory(Entity owner, int capacity, int slotsPerRow = 5)
|
|
{
|
|
this.capacity = capacity;
|
|
|
|
this.Owner = owner;
|
|
|
|
slots = new ItemSlot[capacity];
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
slots[i] = new ItemSlot(this);
|
|
}
|
|
|
|
#if CLIENT
|
|
this.slotsPerRow = slotsPerRow;
|
|
|
|
if (DraggableIndicator == null)
|
|
{
|
|
DraggableIndicator = GUIStyle.GetComponentStyle("GUIDragIndicator").GetDefaultSprite();
|
|
|
|
slotHotkeySprite = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(258, 7, 120, 120), null, 0);
|
|
|
|
EquippedIndicator = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(550, 137, 87, 16), new Vector2(0.5f, 0.5f), 0);
|
|
EquippedHoverIndicator = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(550, 157, 87, 16), new Vector2(0.5f, 0.5f), 0);
|
|
EquippedClickedIndicator = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(550, 177, 87, 16), new Vector2(0.5f, 0.5f), 0);
|
|
|
|
UnequippedIndicator = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(550, 197, 87, 16), new Vector2(0.5f, 0.5f), 0);
|
|
UnequippedHoverIndicator = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(550, 217, 87, 16), new Vector2(0.5f, 0.5f), 0);
|
|
UnequippedClickedIndicator = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(550, 237, 87, 16), new Vector2(0.5f, 0.5f), 0);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public IEnumerable<Item> GetAllItems(bool checkForDuplicates)
|
|
{
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
var items = slots[i].Items;
|
|
// ReSharper disable once ForCanBeConvertedToForeach, because this is performance-sensitive code.
|
|
for (int j = 0; j < items.Count; j++)
|
|
{
|
|
var item = items[j];
|
|
if (item == null)
|
|
{
|
|
#if DEBUG
|
|
DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!");
|
|
#endif
|
|
continue;
|
|
}
|
|
|
|
if (checkForDuplicates)
|
|
{
|
|
bool duplicateFound = false;
|
|
for (int s = 0; s < i; s++)
|
|
{
|
|
if (slots[s].Items.Contains(item))
|
|
{
|
|
duplicateFound = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!duplicateFound) { yield return item; }
|
|
}
|
|
else
|
|
{
|
|
yield return item;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void NotifyItemComponentsOfChange()
|
|
{
|
|
#if CLIENT
|
|
if (Owner is Character character && character == Character.Controlled)
|
|
{
|
|
character.SelectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
|
|
}
|
|
#endif
|
|
|
|
if (Owner is not Item it) { return; }
|
|
|
|
// Use ToArray() snapshot for thread-safe iteration
|
|
foreach (var c in it.Components.ToArray())
|
|
{
|
|
c.OnInventoryChanged();
|
|
}
|
|
|
|
it.ParentInventory?.NotifyItemComponentsOfChange();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the item contained in this inventory. Does not recursively check items inside items.
|
|
/// </summary>
|
|
public bool Contains(Item item)
|
|
{
|
|
return slots.Any(i => i.Contains(item));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the first item in the inventory, or null if the inventory is empty.
|
|
/// </summary>
|
|
public Item FirstOrDefault()
|
|
{
|
|
foreach (var itemSlot in slots)
|
|
{
|
|
var item = itemSlot.FirstOrDefault();
|
|
if (item != null) { return item; }
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the last item in the inventory, or null if the inventory is empty.
|
|
/// </summary>
|
|
public Item LastOrDefault()
|
|
{
|
|
for (int i = slots.Length - 1; i >= 0; i--)
|
|
{
|
|
var item = slots[i].LastOrDefault();
|
|
if (item != null) { return item; }
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private bool IsIndexInRange(int index)
|
|
{
|
|
return index >= 0 && index < slots.Length;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the item stored in the specified inventory slot. If the slot contains a stack of items, returns the first item in the stack.
|
|
/// </summary>
|
|
public Item GetItemAt(int index)
|
|
{
|
|
if (!IsIndexInRange(index)) { return null; }
|
|
return slots[index].FirstOrDefault();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all the item stored in the specified inventory slot. Can return more than one item if the slot contains a stack of items.
|
|
/// </summary>
|
|
public IEnumerable<Item> GetItemsAt(int index)
|
|
{
|
|
if (!IsIndexInRange(index)) { return Enumerable.Empty<Item>(); }
|
|
return slots[index].Items;
|
|
}
|
|
|
|
public int GetItemStackSlotIndex(Item item, int index)
|
|
{
|
|
if (!IsIndexInRange(index)) { return -1; }
|
|
return slots[index].Items.IndexOf(item);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the index of the first slot the item is contained in.
|
|
/// </summary>
|
|
public int FindIndex(Item item)
|
|
{
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
if (slots[i].Contains(item)) { return i; }
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the indices of all the slots the item is contained in (two-hand items for example can be in multiple slots). Note that this method instantiates a new list.
|
|
/// </summary>
|
|
public List<int> FindIndices(Item item)
|
|
{
|
|
List<int> indices = new List<int>();
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
if (slots[i].Contains(item)) { indices.Add(i); }
|
|
}
|
|
return indices;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the item owns any of the parent inventories.
|
|
/// </summary>
|
|
public virtual bool ItemOwnsSelf(Item item)
|
|
{
|
|
if (Owner == null) { return false; }
|
|
if (Owner is not Item) { return false; }
|
|
Item ownerItem = Owner as Item;
|
|
if (ownerItem == item) { return true; }
|
|
if (ownerItem.ParentInventory == null) { return false; }
|
|
return ownerItem.ParentInventory.ItemOwnsSelf(item);
|
|
}
|
|
|
|
public virtual int FindAllowedSlot(Item item, bool ignoreCondition = false)
|
|
{
|
|
if (ItemOwnsSelf(item)) { return -1; }
|
|
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
//item is already in the inventory!
|
|
if (slots[i].Contains(item)) { return -1; }
|
|
}
|
|
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
if (slots[i].CanBePut(item, ignoreCondition)) { return i; }
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can the item be put in the inventory (i.e. is there a suitable free slot or a stack the item can be put in).
|
|
/// </summary>
|
|
public bool CanBePut(Item item)
|
|
{
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
if (CanBePutInSlot(item, i)) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can the item be put in the specified slot.
|
|
/// </summary>
|
|
public virtual bool CanBePutInSlot(Item item, int i, bool ignoreCondition = false)
|
|
{
|
|
if (ItemOwnsSelf(item)) { return false; }
|
|
if (!IsIndexInRange(i)) { return false; }
|
|
return slots[i].CanBePut(item, ignoreCondition);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can an instance of the item prefab be put into the inventory?
|
|
/// Note that if the condition and quality aren't given, they are ignored, and the method can return true even if the item can't go in the inventory
|
|
/// due to the slots being occupied by another item with a different condition or quality (which disallows stacking).
|
|
/// </summary>
|
|
public bool CanProbablyBePut(ItemPrefab itemPrefab, float? condition = null, int? quality = null)
|
|
{
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
if (CanBePutInSlot(itemPrefab, i, condition, quality)) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public virtual bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition = null, int? quality = null)
|
|
{
|
|
if (!IsIndexInRange(i)) { return false; }
|
|
return slots[i].CanProbablyBePut(itemPrefab, condition, quality);
|
|
}
|
|
|
|
public int HowManyCanBePut(ItemPrefab itemPrefab, float? condition = null)
|
|
{
|
|
int count = 0;
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
count += HowManyCanBePut(itemPrefab, i, condition);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition, bool ignoreItemsInSlot = false)
|
|
{
|
|
if (!IsIndexInRange(i)) { return 0; }
|
|
return slots[i].HowManyCanBePut(itemPrefab, condition: condition, ignoreItemsInSlot: ignoreItemsInSlot);
|
|
}
|
|
|
|
/// <summary>
|
|
/// If there is room, puts the item in the inventory and returns true, otherwise returns false
|
|
/// </summary>
|
|
public virtual bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false)
|
|
{
|
|
int slot = FindAllowedSlot(item, ignoreCondition);
|
|
if (slot < 0) { return false; }
|
|
|
|
PutItem(item, slot, user, true, createNetworkEvent);
|
|
return true;
|
|
}
|
|
|
|
public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false)
|
|
{
|
|
if (!IsIndexInRange(i))
|
|
{
|
|
string thisItemStr = item?.Prefab.Identifier.Value ?? "null";
|
|
string ownerStr = "null";
|
|
if (Owner is Item ownerItem)
|
|
{
|
|
ownerStr = ownerItem.Prefab.Identifier.Value;
|
|
}
|
|
else if (Owner is Character ownerCharacter)
|
|
{
|
|
ownerStr = ownerCharacter.SpeciesName.Value;
|
|
}
|
|
string errorMsg = $"Inventory.TryPutItem failed: index was out of range (item: {thisItemStr}, inventory: {ownerStr}).";
|
|
GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
|
#if DEBUG
|
|
DebugConsole.ThrowError(errorMsg);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
if (Owner == null) return false;
|
|
//there's already an item in the slot
|
|
if (slots[i].Any() && allowCombine)
|
|
{
|
|
if (slots[i].First().Combine(item, user))
|
|
{
|
|
//item in the slot removed as a result of combining -> put this item in the now free slot
|
|
if (!slots[i].Any())
|
|
{
|
|
return TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
if (CanBePutInSlot(item, i, ignoreCondition))
|
|
{
|
|
PutItem(item, i, user, true, createNetworkEvent);
|
|
return true;
|
|
}
|
|
else if (slots[i].Any() && item.ParentInventory != null && allowSwapping)
|
|
{
|
|
var itemInSlot = slots[i].First();
|
|
if (itemInSlot.OwnInventory != null &&
|
|
!itemInSlot.OwnInventory.Contains(item) &&
|
|
itemInSlot.GetComponent<ItemContainer>()?.GetMaxStackSize(0) == 1 &&
|
|
itemInSlot.OwnInventory.TrySwapping(0, item, user, createNetworkEvent, swapWholeStack: false))
|
|
{
|
|
return true;
|
|
}
|
|
return
|
|
TrySwapping(i, item, user, createNetworkEvent, swapWholeStack: true) ||
|
|
TrySwapping(i, item, user, createNetworkEvent, swapWholeStack: false);
|
|
}
|
|
else
|
|
{
|
|
#if CLIENT
|
|
if (visualSlots != null && createNetworkEvent) { visualSlots[i].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); }
|
|
#endif
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true)
|
|
{
|
|
if (!IsIndexInRange(i))
|
|
{
|
|
string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace();
|
|
GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
|
return;
|
|
}
|
|
|
|
var should = GameMain.LuaCs.Hook.Call<bool?>("inventoryPutItem", this, item, user, i, removeItem);
|
|
|
|
if (should != null && should.Value)
|
|
return;
|
|
|
|
if (Owner == null) { return; }
|
|
|
|
Inventory prevInventory = item.ParentInventory;
|
|
Inventory prevOwnerInventory = item.FindParentInventory(inv => inv is CharacterInventory);
|
|
|
|
if (createNetworkEvent)
|
|
{
|
|
CreateNetworkEvent();
|
|
//also delay syncing the inventory the item was inside
|
|
if (prevInventory != null && prevInventory != this) { prevInventory.syncItemsDelay = 1.0f; }
|
|
}
|
|
|
|
if (removeItem)
|
|
{
|
|
item.Drop(user, setTransform: false, createNetworkEvent: createNetworkEvent && GameMain.NetworkMember is { IsServer: true });
|
|
item.ParentInventory?.RemoveItem(item);
|
|
}
|
|
|
|
slots[i].Add(item);
|
|
item.ParentInventory = this;
|
|
|
|
#if CLIENT
|
|
if (visualSlots != null)
|
|
{
|
|
visualSlots[i].ShowBorderHighlight(Color.White, 0.1f, 0.4f);
|
|
if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; }
|
|
}
|
|
#endif
|
|
CharacterHUD.RecreateHudTextsIfControlling(user);
|
|
|
|
if (item.body != null)
|
|
{
|
|
item.body.Enabled = false;
|
|
item.body.BodyType = FarseerPhysics.BodyType.Dynamic;
|
|
item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false);
|
|
//update to refresh the interpolated draw rotation and position (update doesn't run on disabled bodies)
|
|
item.body.UpdateDrawPosition(interpolate: false);
|
|
}
|
|
|
|
#if SERVER
|
|
if (prevOwnerInventory is CharacterInventory characterInventory && characterInventory != this && Owner == user)
|
|
{
|
|
var client = GameMain.Server?.ConnectedClients?.Find(cl => cl.Character == user);
|
|
GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(characterInventory, client, item);
|
|
}
|
|
#endif
|
|
if (this is CharacterInventory)
|
|
{
|
|
if (prevInventory != this && prevOwnerInventory != this)
|
|
{
|
|
HumanAIController.ItemTaken(item, user);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (item.FindParentInventory(inv => inv is CharacterInventory) is CharacterInventory currentInventory)
|
|
{
|
|
if (currentInventory != prevInventory)
|
|
{
|
|
HumanAIController.ItemTaken(item, user);
|
|
}
|
|
}
|
|
}
|
|
|
|
NotifyItemComponentsOfChange();
|
|
}
|
|
|
|
public bool IsEmpty()
|
|
{
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
if (slots[i].Any()) { return false; }
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is there room to put more items in the inventory. Doesn't take stacking into account by default.
|
|
/// </summary>
|
|
/// <param name="takeStacksIntoAccount">If true, the inventory is not considered full if all the stacks are not full.</param>
|
|
public virtual bool IsFull(bool takeStacksIntoAccount = false)
|
|
{
|
|
if (takeStacksIntoAccount)
|
|
{
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
if (!slots[i].Any()) { return false; }
|
|
var item = slots[i].FirstOrDefault();
|
|
if (slots[i].Items.Count < item.Prefab.GetMaxStackSize(this)) { return false; }
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
if (!slots[i].Any()) { return false; }
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected bool TrySwapping(int index, Item item, Character user, bool createNetworkEvent, bool swapWholeStack)
|
|
{
|
|
if (item?.ParentInventory == null || !slots[index].Any()) { return false; }
|
|
if (slots[index].Items.Any(it => !it.IsInteractable(user))) { return false; }
|
|
if (!AllowSwappingContainedItems) { return false; }
|
|
|
|
var should = GameMain.LuaCs.Hook.Call<bool?>("inventoryItemSwap", this, item, user, index, swapWholeStack);
|
|
|
|
if (should != null)
|
|
return should.Value;
|
|
|
|
//swap to InvSlotType.Any if possible
|
|
Inventory otherInventory = item.ParentInventory;
|
|
bool otherIsEquipped = false;
|
|
int otherIndex = -1;
|
|
for (int i = 0; i < otherInventory.slots.Length; i++)
|
|
{
|
|
if (!otherInventory.slots[i].Contains(item)) { continue; }
|
|
if (otherInventory is CharacterInventory characterInventory)
|
|
{
|
|
if (characterInventory.SlotTypes[i] == InvSlotType.Any)
|
|
{
|
|
otherIndex = i;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
otherIsEquipped = true;
|
|
}
|
|
}
|
|
}
|
|
if (otherIndex == -1)
|
|
{
|
|
otherIndex = otherInventory.FindIndex(item);
|
|
if (otherIndex == -1)
|
|
{
|
|
DebugConsole.ThrowError("Something went wrong when trying to swap items between inventory slots: couldn't find the source item from it's inventory.\n" + Environment.StackTrace.CleanupStackTrace());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
List<Item> existingItems = new List<Item>();
|
|
if (swapWholeStack)
|
|
{
|
|
existingItems.AddRange(slots[index].Items);
|
|
for (int j = 0; j < capacity; j++)
|
|
{
|
|
if (existingItems.Any(existingItem => slots[j].Contains(existingItem))) { slots[j].RemoveAllItems(); }
|
|
}
|
|
}
|
|
else
|
|
{
|
|
existingItems.Add(slots[index].FirstOrDefault());
|
|
for (int j = 0; j < capacity; j++)
|
|
{
|
|
if (existingItems.Any(existingItem => slots[j].Contains(existingItem))) { slots[j].RemoveItem(existingItems.First()); }
|
|
}
|
|
}
|
|
|
|
List<Item> stackedItems = new List<Item>();
|
|
if (swapWholeStack)
|
|
{
|
|
for (int j = 0; j < otherInventory.capacity; j++)
|
|
{
|
|
// in case the item is in multiple slots like OuterClothes | InnerClothes, prevent it from being added twice to the list
|
|
if (otherInventory.slots[j].Contains(item) && !stackedItems.Contains(item))
|
|
{
|
|
stackedItems.AddRange(otherInventory.slots[j].Items);
|
|
otherInventory.slots[j].RemoveAllItems();
|
|
}
|
|
}
|
|
}
|
|
else if (!stackedItems.Contains(item))
|
|
{
|
|
stackedItems.Add(item);
|
|
otherInventory.slots[otherIndex].RemoveItem(item);
|
|
}
|
|
|
|
|
|
bool swapSuccessful = false;
|
|
if (otherIsEquipped)
|
|
{
|
|
swapSuccessful =
|
|
stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent))
|
|
&&
|
|
(existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) ||
|
|
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent));
|
|
}
|
|
else
|
|
{
|
|
swapSuccessful =
|
|
(existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) ||
|
|
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent))
|
|
&&
|
|
stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent));
|
|
|
|
if (!swapSuccessful && existingItems.Count == 1 && existingItems[0].AllowDroppingOnSwapWith(item))
|
|
{
|
|
if (!(existingItems[0].Container?.ParentInventory is CharacterInventory characterInv) ||
|
|
!characterInv.TryPutItem(existingItems[0], user, new List<InvSlotType>() { InvSlotType.Any }))
|
|
{
|
|
existingItems[0].Drop(user, createNetworkEvent);
|
|
}
|
|
swapSuccessful = stackedItems.Distinct().Any(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent));
|
|
#if CLIENT
|
|
if (swapSuccessful)
|
|
{
|
|
SoundPlayer.PlayUISound(GUISoundType.DropItem);
|
|
if (otherInventory.visualSlots != null && otherIndex > -1)
|
|
{
|
|
otherInventory.visualSlots[otherIndex].ShowBorderHighlight(Color.Transparent, 0.1f, 0.1f);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
//if the item in the slot can be moved to the slot of the moved item
|
|
if (swapSuccessful)
|
|
{
|
|
System.Diagnostics.Debug.Assert(slots[index].Contains(item), "Something when wrong when swapping items, item is not present in the inventory.");
|
|
System.Diagnostics.Debug.Assert(!existingItems.Any(it => !it.Prefab.AllowDroppingOnSwap && !otherInventory.Contains(it)), "Something when wrong when swapping items, item is not present in the other inventory.");
|
|
#if CLIENT
|
|
if (visualSlots != null)
|
|
{
|
|
for (int j = 0; j < capacity; j++)
|
|
{
|
|
if (slots[j].Contains(item)) { visualSlots[j].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.9f); }
|
|
}
|
|
}
|
|
if (otherInventory.visualSlots != null)
|
|
{
|
|
for (int j = 0; j < otherInventory.capacity; j++)
|
|
{
|
|
if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) { otherInventory.visualSlots[j].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.9f); }
|
|
}
|
|
}
|
|
#endif
|
|
return true;
|
|
}
|
|
else //swapping the items failed -> move them back to where they were
|
|
{
|
|
if (swapWholeStack)
|
|
{
|
|
foreach (Item stackedItem in stackedItems)
|
|
{
|
|
for (int j = 0; j < capacity; j++)
|
|
{
|
|
if (slots[j].Contains(stackedItem)) { slots[j].RemoveItem(stackedItem); };
|
|
}
|
|
}
|
|
foreach (Item existingItem in existingItems)
|
|
{
|
|
for (int j = 0; j < otherInventory.capacity; j++)
|
|
{
|
|
if (otherInventory.slots[j].Contains(existingItem)) { otherInventory.slots[j].RemoveItem(existingItem); }
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int j = 0; j < capacity; j++)
|
|
{
|
|
if (slots[j].Contains(item))
|
|
{
|
|
slots[j].RemoveWhere(it => existingItems.Contains(it) || stackedItems.Contains(it));
|
|
}
|
|
}
|
|
for (int j = 0; j < otherInventory.capacity; j++)
|
|
{
|
|
if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault()))
|
|
{
|
|
otherInventory.slots[j].RemoveWhere(it => existingItems.Contains(it) || stackedItems.Contains(it));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (otherIsEquipped)
|
|
{
|
|
TryPutAndForce(existingItems, this, index);
|
|
TryPutAndForce(stackedItems, otherInventory, otherIndex);
|
|
}
|
|
else
|
|
{
|
|
TryPutAndForce(stackedItems, otherInventory, otherIndex);
|
|
TryPutAndForce(existingItems, this, index);
|
|
}
|
|
|
|
void TryPutAndForce(IEnumerable<Item> items, Inventory inventory, int slotIndex)
|
|
{
|
|
foreach (var item in items)
|
|
{
|
|
if (!inventory.TryPutItem(item, slotIndex, false, false, user, createNetworkEvent, ignoreCondition: true) &&
|
|
!inventory.GetItemsAt(slotIndex).Contains(item))
|
|
{
|
|
inventory.ForceToSlot(item, slotIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (createNetworkEvent)
|
|
{
|
|
CreateNetworkEvent();
|
|
otherInventory.CreateNetworkEvent();
|
|
}
|
|
|
|
#if CLIENT
|
|
if (visualSlots != null)
|
|
{
|
|
for (int j = 0; j < capacity; j++)
|
|
{
|
|
if (slots[j].Contains(existingItems.FirstOrDefault()))
|
|
{
|
|
visualSlots[j].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void CreateNetworkEvent()
|
|
{
|
|
if (GameMain.NetworkMember == null) { return; }
|
|
if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; }
|
|
|
|
//split into multiple events because one might not be enough to fit all the items
|
|
List<Range> slotRanges = new List<Range>();
|
|
int startIndex = 0;
|
|
int itemCount = 0;
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
int count = slots[i].Items.Count;
|
|
if (itemCount + count > MaxItemsPerNetworkEvent || i == capacity - 1)
|
|
{
|
|
slotRanges.Add(new Range(startIndex, i + 1));
|
|
startIndex = i + 1;
|
|
itemCount = 0;
|
|
}
|
|
itemCount += count;
|
|
}
|
|
|
|
foreach (var slotRange in slotRanges)
|
|
{
|
|
CreateNetworkEvent(slotRange);
|
|
}
|
|
}
|
|
|
|
protected virtual void CreateNetworkEvent(Range slotRange) { }
|
|
|
|
public Item FindItem(Func<Item, bool> predicate, bool recursive)
|
|
{
|
|
Item match = AllItems.FirstOrDefault(predicate);
|
|
if (match == null && recursive)
|
|
{
|
|
foreach (var item in AllItems)
|
|
{
|
|
if (item?.OwnInventory == null) { continue; }
|
|
|
|
match = item.OwnInventory.FindItem(predicate, recursive: true);
|
|
if (match != null) { return match; }
|
|
}
|
|
}
|
|
return match;
|
|
}
|
|
|
|
public List<Item> FindAllItems(Func<Item, bool> predicate = null, bool recursive = false, List<Item> list = null)
|
|
{
|
|
list ??= new List<Item>();
|
|
foreach (var item in AllItems)
|
|
{
|
|
if (predicate == null || predicate(item))
|
|
{
|
|
list.Add(item);
|
|
}
|
|
if (recursive)
|
|
{
|
|
item.OwnInventory?.FindAllItems(predicate, recursive: true, list);
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
|
|
public Item FindItemByTag(Identifier tag, bool recursive = false)
|
|
{
|
|
if (tag.IsEmpty) { return null; }
|
|
return FindItem(i => i.HasTag(tag), recursive);
|
|
}
|
|
|
|
public Item FindItemByIdentifier(Identifier identifier, bool recursive = false)
|
|
{
|
|
if (identifier.IsEmpty) { return null; }
|
|
return FindItem(i => i.Prefab.Identifier == identifier, recursive);
|
|
}
|
|
|
|
public virtual void RemoveItem(Item item)
|
|
{
|
|
if (item == null) { return; }
|
|
|
|
//go through the inventory and remove the item from all slots
|
|
for (int n = 0; n < capacity; n++)
|
|
{
|
|
if (!slots[n].Contains(item)) { continue; }
|
|
|
|
slots[n].RemoveItem(item);
|
|
item.ParentInventory = null;
|
|
#if CLIENT
|
|
if (visualSlots != null)
|
|
{
|
|
visualSlots[n].ShowBorderHighlight(Color.White, 0.1f, 0.4f);
|
|
if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; }
|
|
}
|
|
#endif
|
|
CharacterHUD.RecreateHudTextsIfFocused(item);
|
|
}
|
|
|
|
NotifyItemComponentsOfChange();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Forces an item to a specific slot. Doesn't remove the item from existing slots/inventories or do any other sanity checks, use with caution!
|
|
/// </summary>
|
|
public void ForceToSlot(Item item, int index)
|
|
{
|
|
slots[index].Add(item);
|
|
item.ParentInventory = this;
|
|
bool equipped = (this as CharacterInventory)?.Owner is Character character && character.HasEquippedItem(item);
|
|
if (item.body != null && !equipped)
|
|
{
|
|
item.body.Enabled = false;
|
|
item.body.BodyType = FarseerPhysics.BodyType.Dynamic;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes an item from a specific slot. Doesn't do any sanity checks, use with caution!
|
|
/// </summary>
|
|
public void ForceRemoveFromSlot(Item item, int index)
|
|
{
|
|
slots[index].RemoveItem(item);
|
|
}
|
|
|
|
public bool IsInSlot(Item item, int index)
|
|
{
|
|
if (!IsIndexInRange(index)) { return false; }
|
|
return slots[index].Contains(item);
|
|
}
|
|
|
|
public bool IsSlotEmpty(int index)
|
|
{
|
|
if (!IsIndexInRange(index)) { return false; }
|
|
return slots[index].Empty();
|
|
}
|
|
|
|
public void SharedRead(IReadMessage msg, List<ushort>[] receivedItemIds, out bool readyToApply)
|
|
{
|
|
byte start = msg.ReadByte();
|
|
byte end = msg.ReadByte();
|
|
|
|
//if we received the first chunk of item IDs, clear the rest
|
|
//to ensure we don't have anything outdated in the list - we're about to receive the rest of the IDs next
|
|
if (start == 0)
|
|
{
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
receivedItemIds[i] = null;
|
|
}
|
|
}
|
|
|
|
for (int i = start; i < end; i++)
|
|
{
|
|
var newItemIds = new List<ushort>();
|
|
int itemCount = msg.ReadRangedInteger(0, MaxPossibleStackSize);
|
|
for (int j = 0; j < itemCount; j++)
|
|
{
|
|
newItemIds.Add(msg.ReadUInt16());
|
|
}
|
|
receivedItemIds[i] = newItemIds;
|
|
}
|
|
|
|
//if all IDs haven't been received yet (chunked into multiple events?)
|
|
//don't apply the state yet
|
|
readyToApply = !receivedItemIds.Contains(null);
|
|
}
|
|
|
|
public void SharedWrite(IWriteMessage msg, Range slotRange)
|
|
{
|
|
int start = slotRange.Start.Value;
|
|
int end = slotRange.End.Value;
|
|
|
|
msg.WriteByte((byte)start);
|
|
msg.WriteByte((byte)end);
|
|
for (int i = start; i < end; i++)
|
|
{
|
|
msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxPossibleStackSize);
|
|
for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxPossibleStackSize); j++)
|
|
{
|
|
var item = slots[i].Items[j];
|
|
msg.WriteUInt16(item?.ID ?? (ushort)0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes all items inside the inventory (and also recursively all items inside the items)
|
|
/// </summary>
|
|
public void DeleteAllItems()
|
|
{
|
|
for (int i = 0; i < capacity; i++)
|
|
{
|
|
if (!slots[i].Any()) { continue; }
|
|
foreach (Item item in slots[i].Items)
|
|
{
|
|
foreach (ItemContainer itemContainer in item.GetComponents<ItemContainer>())
|
|
{
|
|
itemContainer.Inventory.DeleteAllItems();
|
|
}
|
|
}
|
|
slots[i].Items.ForEachMod(it => it.Remove());
|
|
slots[i].RemoveAllItems();
|
|
}
|
|
}
|
|
}
|
|
}
|