Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs
2026-04-30 21:59:54 +08:00

1179 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, bool triggerOnInsertedEffects = true)
{
int slot = FindAllowedSlot(item, ignoreCondition);
if (slot < 0) { return false; }
PutItem(item, slot, user, true, createNetworkEvent, triggerOnInsertedEffects);
return true;
}
public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true)
{
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, triggerOnInsertedEffects);
}
return true;
}
}
if (CanBePutInSlot(item, i, ignoreCondition))
{
PutItem(item, i, user, true, createNetworkEvent, triggerOnInsertedEffects);
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, bool triggerOnInsertedEffects = 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;
}
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; }
//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)
{
//don't trigger OnInserted effects: we're not really "inserting" but just putting it back where it was because swapping failed
if (!inventory.TryPutItem(item, slotIndex, false, false, user, createNetworkEvent, ignoreCondition: true, triggerOnInsertedEffects: false) &&
!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();
}
}
}
}