1187 lines
44 KiB
C#
1187 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; }
|
|
|
|
foreach (var c in it.Components)
|
|
{
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|