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 items = new List(MaxPossibleStackSize); public bool HideIfEmpty; public IReadOnlyList 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; } public bool CanBePut(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; } /// Defaults to if null 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); } /// /// Removes one item from the slot /// 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); } /// /// Removes all items from the slot /// public void RemoveAllItems() { items.Clear(); } public void RemoveWhere(Func 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; /// /// Capacity, or the number of slots in the inventory. /// 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); } /// /// 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. /// public virtual IEnumerable AllItems { get { return GetAllItems(checkForDuplicates: this is CharacterInventory); } } private readonly List allItemsList = new List(); /// /// All items contained in the inventory. Allows modifying the contents of the inventory while being enumerated. /// public IEnumerable 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 } protected IEnumerable GetAllItems(bool checkForDuplicates) { for (int i = 0; i < capacity; i++) { foreach (var item in slots[i].Items) { 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 j = 0; j < i; j++) { if (slots[j].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()?.OnViewUpdateProjSpecific(); } #endif if (Owner is not Item it) { return; } foreach (var c in it.Components) { c.OnInventoryChanged(); } it.ParentInventory?.NotifyItemComponentsOfChange(); } /// /// Is the item contained in this inventory. Does not recursively check items inside items. /// public bool Contains(Item item) { return slots.Any(i => i.Contains(item)); } /// /// Return the first item in the inventory, or null if the inventory is empty. /// public Item FirstOrDefault() { foreach (var itemSlot in slots) { var item = itemSlot.FirstOrDefault(); if (item != null) { return item; } } return null; } /// /// Return the last item in the inventory, or null if the inventory is empty. /// public Item LastOrDefault() { for (int i = slots.Length - 1; i >= 0; i--) { var item = slots[i].LastOrDefault(); if (item != null) { return item; } } return null; } /// /// Get the item stored in the specified inventory slot. If the slot contains a stack of items, returns the first item in the stack. /// public Item GetItemAt(int index) { if (index < 0 || index >= slots.Length) { return null; } return slots[index].FirstOrDefault(); } /// /// Get all the item stored in the specified inventory slot. Can return more than one item if the slot contains a stack of items. /// public IEnumerable GetItemsAt(int index) { if (index < 0 || index >= slots.Length) { return Enumerable.Empty(); } return slots[index].Items; } public int GetItemStackSlotIndex(Item item, int index) { if (index < 0 || index >= slots.Length) { return -1; } return slots[index].Items.IndexOf(item); } /// /// Find the index of the first slot the item is contained in. /// public int FindIndex(Item item) { for (int i = 0; i < capacity; i++) { if (slots[i].Contains(item)) { return i; } } return -1; } /// /// 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. /// public List FindIndices(Item item) { List indices = new List(); for (int i = 0; i < capacity; i++) { if (slots[i].Contains(item)) { indices.Add(i); } } return indices; } /// /// Returns true if the item owns any of the parent inventories. /// public virtual bool ItemOwnsSelf(Item item) { if (Owner == null) { return false; } if (!(Owner is 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; } /// /// 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). /// public bool CanBePut(Item item) { for (int i = 0; i < capacity; i++) { if (CanBePutInSlot(item, i)) { return true; } } return false; } /// /// Can the item be put in the specified slot. /// public virtual bool CanBePutInSlot(Item item, int i, bool ignoreCondition = false) { if (ItemOwnsSelf(item)) { return false; } if (i < 0 || i >= slots.Length) { return false; } return slots[i].CanBePut(item, ignoreCondition); } public bool CanBePut(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 (i < 0 || i >= slots.Length) { return false; } return slots[i].CanBePut(itemPrefab, condition); } 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 (i < 0 || i >= slots.Length) { return 0; } return slots[i].HowManyCanBePut(itemPrefab, condition: condition, ignoreItemsInSlot: ignoreItemsInSlot); } /// /// If there is room, puts the item in the inventory and returns true, otherwise returns false /// public virtual bool TryPutItem(Item item, Character user, IEnumerable 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 (i < 0 || i >= slots.Length) { 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()?.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 (i < 0 || i >= slots.Length) { 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; } /// /// Is there room to put more items in the inventory. Doesn't take stacking into account by default. /// /// If true, the inventory is not considered full if all the stacks are not full. 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 existingItems = new List(); 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 stackedItems = new List(); if (swapWholeStack) { for (int j = 0; j < otherInventory.capacity; j++) { if (otherInventory.slots[j].Contains(item)) { stackedItems.AddRange(otherInventory.slots[j].Items); otherInventory.slots[j].RemoveAllItems(); } } } else { 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.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 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 slotRanges = new List(); 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 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 FindAllItems(Func predicate = null, bool recursive = false, List list = null) { list ??= new List(); 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(); } /// /// 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! /// 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; } } /// /// Removes an item from a specific slot. Doesn't do any sanity checks, use with caution! /// public void ForceRemoveFromSlot(Item item, int index) { slots[index].RemoveItem(item); } public bool IsInSlot(Item item, int index) { if (index < 0 || index >= slots.Length) { return false; } return slots[index].Contains(item); } public void SharedRead(IReadMessage msg, List[] 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(); 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); } } } /// /// Deletes all items inside the inventory (and also recursively all items inside the items) /// 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.Inventory.DeleteAllItems(); } } slots[i].Items.ForEachMod(it => it.Remove()); slots[i].RemoveAllItems(); } } } }