diff --git a/Projects/Server.Tests/Tests/Items/ContainerTests.cs b/Projects/Server.Tests/Tests/Items/ContainerTests.cs new file mode 100644 index 000000000..d500240df --- /dev/null +++ b/Projects/Server.Tests/Tests/Items/ContainerTests.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using Server.Items; +using Xunit; + +namespace Server.Tests; + +public class ContainerTests : IClassFixture +{ + [Fact] + public void TestFindItemsByType() + { + var staticSerial = (Serial)0x3; + + var container = new Container((Serial)0x1); + container.AddItem(new Item((Serial)0x2)); + container.AddItem(new Static(staticSerial)); + + Static staticItem = null; + foreach (var item in container.FindItemsByType()) + { + staticItem = item; + } + + Assert.NotNull(staticItem); + Assert.Equal(staticSerial, staticItem.Serial); + } + + [Fact] + public void TestFindItemsByTypeNested() + { + var static1 = new Static((Serial)0x3); + var static2 = new Static((Serial)0x6); + + var container = new Container((Serial)0x1); + container.AddItem(new Item((Serial)0x2)); + var container2 = new Container((Serial)0x4); + container.AddItem(container2); + + var container3 = new Container((Serial)0x5); + container2.AddItem(container3); + container3.AddItem(static2); + + container2.AddItem(static1); + + List statics = new List(); + foreach (var item in container.FindItemsByType()) + { + statics.Add(item); + } + + Assert.Equal(2, statics.Count); + Assert.Equal(static1, statics[0]); + Assert.Equal(static2, statics[1]); + } + + [Fact] + public void TestFindItemsByTypeNotMatching() + { + var container = new Container((Serial)0x1); + container.AddItem(new Item((Serial)0x2)); + var container2 = new Container((Serial)0x4); + container.AddItem(container2); + container2.AddItem(new Item((Serial)0x5)); + + Static staticItem = null; + foreach (var item in container.FindItemsByType()) + { + staticItem = item; + } + + Assert.Null(staticItem); + } +} diff --git a/Projects/Server/Items/Container.Enumerable.cs b/Projects/Server/Items/Container.Enumerable.cs new file mode 100644 index 000000000..e02529a3a --- /dev/null +++ b/Projects/Server/Items/Container.Enumerable.cs @@ -0,0 +1,243 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2023 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: Container.Enumerable.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Server.Collections; + +namespace Server.Items; + +public partial class Container +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FindItemsByTypeEnumerator FindItemsByType(bool recurse = true, Predicate predicate = null) + => FindItemsByType(recurse, predicate); + + /// + /// Performs a breadth-first search through all the s and + /// nested s within this . + /// + /// + /// DO NOT consume, delete, or move items while iterating + /// + /// + /// + /// var total = 0; + /// foreach (var gold in cont.FindItemsByType<Gold>()) + /// { + /// total += gold.Amount; + /// } + /// + /// + /// Type of objects being searched for + /// + /// Optional: If true, the search will recursively + /// check any nested s; otherwise, nested + /// s will not be searched. + /// + /// + /// Optional: A predicate to check if the + /// of type is one of the targets of the search. + /// + /// + /// An enumerator for iterating through s of type that match the optional + /// . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FindItemsByTypeEnumerator FindItemsByType(bool recurse = true, Predicate predicate = null) + where T : Item => new(this, recurse, predicate); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public QueuedItemsEnumerator EnumerateItemsByType(bool recurse = true, Predicate predicate = null) + => EnumerateItemsByType(recurse, predicate); + + /// + /// Safely enumerates items using a breadth-first search through all the s and + /// nested s within this . + /// + /// + /// Use EnumerateItemsByType for situations where the item might be manipulated, consumed, or moved. + /// Note: This method scans through the container before returning the enumerator for iteration. + /// + /// + /// + /// foreach (var item in cont.EnumerateItemsByType<Item>()) + /// { + /// if (item.LootType is not LootType.Blessed) + /// { + /// item.Delete(); + /// } + /// } + /// + /// + /// Type of objects being searched for + /// + /// Optional: If true, the search will recursively + /// check any nested s; otherwise, nested + /// s will not be searched. + /// + /// + /// Optional: A predicate to check if the + /// of type is one of the targets of the search. + /// + /// + /// An enumerator for iterating through s of type that match the optional + /// . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public QueuedItemsEnumerator EnumerateItemsByType(bool recurse = true, Predicate predicate = null) + where T : Item => new(QueueItemsByType(recurse, predicate)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public PooledRefQueue QueueItemsByType(bool recurse = true, Predicate predicate = null) => + QueueItemsByType(recurse, predicate); + + public PooledRefQueue QueueItemsByType(bool recurse = true, Predicate predicate = null) where T : Item + { + var queue = PooledRefQueue.Create(); + foreach (var item in FindItemsByType(recurse, predicate)) + { + queue.Enqueue(item); + } + + return queue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public PooledRefList ListItemsByType(bool recurse = true, Predicate predicate = null) => + ListItemsByType(recurse, predicate); + + public PooledRefList ListItemsByType(bool recurse = true, Predicate predicate = null) where T : Item + { + var list = PooledRefList.Create(); + foreach (var item in FindItemsByType(recurse, predicate)) + { + list.Add(item); + } + + return list; + } + + public ref struct FindItemsByTypeEnumerator where T : Item + { + private PooledRefQueue _containers; + private Span _items; + private int _index; + private T _current; + private bool _recurse; + private Predicate _predicate; + + public FindItemsByTypeEnumerator(Container container, bool recurse, Predicate predicate) + { + _containers = PooledRefQueue.Create(); + + if (container?.m_Items != null) + { + _items = CollectionsMarshal.AsSpan(container.m_Items); + } + + _current = default; + _index = 0; + _recurse = recurse; + _predicate = predicate; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() => SetNextItem() || _recurse && SetNextContainer(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool SetNextContainer() + { + if (!_containers.TryDequeue(out var c)) + { + return false; + } + + _items = CollectionsMarshal.AsSpan(c.m_Items); + _index = 0; + return SetNextItem(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool SetNextItem() + { + while (_index < _items.Length) + { + Item item = _items[_index++]; + if (_recurse && item is Container { m_Items.Count: > 0 } c) + { + _containers.Enqueue(c); + } + + if (item is T t && _predicate?.Invoke(t) != false) + { + _current = t; + return true; + } + } + + return false; + } + + public T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _current; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() => _containers.Dispose(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FindItemsByTypeEnumerator GetEnumerator() => this; + } + + public ref struct QueuedItemsEnumerator where T : Item + { + private PooledRefQueue _queue; + private T _current; + + public QueuedItemsEnumerator(PooledRefQueue queue) + { + _queue = queue; + _current = default; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (_queue.TryDequeue(out var item)) + { + _current = item; + return true; + } + + return false; + } + + public T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _current; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() => _queue.Dispose(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public QueuedItemsEnumerator GetEnumerator() => this; + } +} diff --git a/Projects/Server/Items/Container.cs b/Projects/Server/Items/Container.cs index a86ab89e0..6e5fc6dfe 100644 --- a/Projects/Server/Items/Container.cs +++ b/Projects/Server/Items/Container.cs @@ -14,10 +14,8 @@ public delegate int CheckItemGroup(Item a, Item b); public delegate void ContainerSnoopHandler(Container cont, Mobile from); -public class Container : Item +public partial class Container : Item { - private static readonly List m_FindItemsList = new(); - private ContainerData m_ContainerData; private int m_DropSound; @@ -817,19 +815,26 @@ public class Container : Item throw new ArgumentNullException(nameof(grouper)); } - var typedItems = CollectionsMarshal.AsSpan(FindItemsByType(type, recurse)); + using var typedItems = PooledRefList.Create(); + foreach (var item in FindItemsByType(recurse)) + { + if (type.IsInstanceOfType(item)) + { + typedItems.Add(item); + } + } var groups = new List>(); var idx = 0; - while (idx < typedItems.Length) + while (idx < typedItems.Count) { var a = typedItems[idx++]; var group = new List(); group.Add(a); - while (idx < typedItems.Length) + while (idx < typedItems.Count) { var b = typedItems[idx]; var v = grouper(a, b); @@ -929,19 +934,27 @@ public class Container : Item for (var i = 0; i < types.Length; ++i) { - var typedItems = CollectionsMarshal.AsSpan(FindItemsByType(types[i], recurse)); + var type = types[i]; + using var typedItems = PooledRefList.Create(); + foreach (var item in FindItemsByType(recurse)) + { + if (type.IsInstanceOfType(item)) + { + typedItems.Add(item); + } + } var groups = new List>(); var idx = 0; - while (idx < typedItems.Length) + while (idx < typedItems.Count) { var a = typedItems[idx++]; var group = new List(); group.Add(a); - while (idx < typedItems.Length) + while (idx < typedItems.Count) { var b = typedItems[idx]; var v = grouper(a, b); @@ -1253,25 +1266,31 @@ public class Container : Item public bool ConsumeTotal(Type type, int amount = 1, bool recurse = true, OnItemConsumed callback = null) { - var items = CollectionsMarshal.AsSpan(FindItemsByType(type, recurse)); + var total = 0; + using var items = PooledRefQueue.Create(); // First pass, compute total - var total = 0; - - for (var i = 0; i < items.Length; ++i) + foreach (var item in FindItemsByType(recurse)) { - total += items[i].Amount; + if (type.IsInstanceOfType(item)) + { + Items.Add(item); + total += item.Amount; + if (total >= amount) + { + break; + } + } } + // We have enough, so consume it if (total >= amount) { - // We've enough, so consume it - var need = amount; - for (var i = 0; i < items.Length; ++i) + while (items.Count > 0) { - var item = items[i]; + var item = items.Dequeue(); var theirAmount = item.Amount; @@ -1287,7 +1306,6 @@ public class Container : Item callback?.Invoke(item, need); item.Consume(need); - return true; } } @@ -1361,12 +1379,19 @@ public class Container : Item var best = 0; - var typedItems = CollectionsMarshal.AsSpan(FindItemsByType(type, recurse)); + using var typedItems = PooledRefList.Create(); + foreach (var item in FindItemsByType(recurse)) + { + if (type.IsInstanceOfType(item)) + { + typedItems.Add(item); + } + } var groups = new List>(); var idx = 0; - while (idx < typedItems.Length) + while (idx < typedItems.Count) { var a = typedItems[idx++]; var group = new List @@ -1374,7 +1399,7 @@ public class Container : Item a }; - while (idx < typedItems.Length) + while (idx < typedItems.Count) { var b = typedItems[idx]; var v = grouper(a, b); @@ -1540,9 +1565,12 @@ public class Container : Item public int GetAmount(Type type, bool recurse = true) { var total = 0; - foreach (var item in FindItemsByType(type, recurse)) + foreach (var item in FindItemsByType(recurse)) { - total += item.Amount; + if (type.IsInstanceOfType(item)) + { + total += item.Amount; + } } return total; @@ -1551,9 +1579,12 @@ public class Container : Item public int GetAmount(Type[] types, bool recurse = true) { var total = 0; - foreach (var item in FindItemsByType(types, recurse)) + foreach (var item in FindItemsByType(recurse)) { - total += item.Amount; + if (InTypeList(item, types)) + { + total += item.Amount; + } } return total; @@ -1561,23 +1592,12 @@ public class Container : Item public List FindItemsByType(Type type, bool recurse = true) { - using var queue = PooledRefQueue.Create(128); - queue.Enqueue(this); var items = new List(); - while (queue.Count > 0) + foreach (var item in FindItemsByType(recurse)) { - var container = queue.Dequeue(); - foreach (var item in container.Items) + if (type.IsInstanceOfType(item)) { - if (type.IsInstanceOfType(item)) - { - items.Add(item); - } - - if (recurse && item is Container itemContainer) - { - queue.Enqueue(itemContainer); - } + items.Add(item); } } @@ -1586,23 +1606,12 @@ public class Container : Item public List FindItemsByType(Type[] types, bool recurse = true) { - using var queue = PooledRefQueue.Create(128); - queue.Enqueue(this); var items = new List(); - while (queue.Count > 0) + foreach (var item in FindItemsByType(recurse)) { - var container = queue.Dequeue(); - foreach (var item in container.Items) + if (InTypeList(item, types)) { - if (InTypeList(item, types)) - { - items.Add(item); - } - - if (recurse && item is Container itemContainer) - { - queue.Enqueue(itemContainer); - } + items.Add(item); } } @@ -1611,22 +1620,11 @@ public class Container : Item public Item FindItemByType(Type type, bool recurse = true) { - using var queue = PooledRefQueue.Create(128); - queue.Enqueue(this); - while (queue.Count > 0) + foreach (var item in FindItemsByType(recurse)) { - var container = queue.Dequeue(); - foreach (var item in container.Items) + if (type.IsInstanceOfType(item)) { - if (type.IsInstanceOfType(item)) - { - return item; - } - - if (recurse && item is Container itemContainer) - { - queue.Enqueue(itemContainer); - } + return item; } } @@ -1635,73 +1633,17 @@ public class Container : Item public Item FindItemByType(Type[] types, bool recurse = true) { - using var queue = PooledRefQueue.Create(128); - queue.Enqueue(this); - while (queue.Count > 0) + foreach (var item in FindItemsByType(recurse)) { - var container = queue.Dequeue(); - foreach (var item in container.Items) + if (InTypeList(item, types)) { - if (InTypeList(item, types)) - { - return item; - } - - if (recurse && item is Container itemContainer) - { - queue.Enqueue(itemContainer); - } + return item; } } return null; } - public List FindItemsByType(Predicate predicate) where T : Item => FindItemsByType(true, predicate); - - /// - /// Performs a Breadth-First search through all the s and - /// nested s within this . - /// - /// Type of objects being searched for - /// - /// Optional: If true, the search will recursively - /// check any nested s; otherwise, nested - /// s will not be searched. - /// - /// - /// Optional: A predicate to check if the - /// of type is one of the targets of the search. - /// - /// - /// A list of s of type that matche the optional - /// . - /// - public List FindItemsByType(bool recurse = true, Predicate predicate = null) where T : Item - { - using var queue = PooledRefQueue.Create(128); - queue.Enqueue(this); - var items = new List(); - while (queue.Count > 0) - { - var container = queue.Dequeue(); - foreach (var item in container.Items) - { - if (item is T typedItem && predicate?.Invoke(typedItem) != false) - { - items.Add(typedItem); - } - - if (recurse && item is Container itemContainer) - { - queue.Enqueue(itemContainer); - } - } - } - - return items; - } - /// /// Performs a Breadth-First search through all the s and /// nested s within this . @@ -1722,37 +1664,14 @@ public class Container : Item /// public T FindItemByType(bool recurse = true, Predicate predicate = null) where T : Item { - using var queue = PooledRefQueue.Create(128); - queue.Enqueue(this); - while (queue.Count > 0) + foreach (var item in FindItemsByType(recurse, predicate)) { - var container = queue.Dequeue(); - foreach (var item in container.Items) - { - if (item is T typedItem && predicate?.Invoke(typedItem) != false) - { - return typedItem; - } - - if (recurse && item is Container itemContainer) - { - queue.Enqueue(itemContainer); - } - } + return item; } return null; } - private class GroupComparer : IComparer - { - private readonly CheckItemGroup m_Grouper; - - public GroupComparer(CheckItemGroup grouper) => m_Grouper = grouper; - - public int Compare(Item a, Item b) => m_Grouper(a, b); - } - [Flags] private enum SaveFlag : byte { diff --git a/Projects/Server/Mobiles/IMount.cs b/Projects/Server/Mobiles/IMount.cs index 9ce95e692..82f5b6932 100644 --- a/Projects/Server/Mobiles/IMount.cs +++ b/Projects/Server/Mobiles/IMount.cs @@ -13,8 +13,6 @@ * along with this program. If not, see . * *************************************************************************/ -using System; - namespace Server.Mobiles; public interface IMount : IHasSteps diff --git a/Projects/UOContent/Commands/Generic/Implementors/ContainedCommandImplementor.cs b/Projects/UOContent/Commands/Generic/Implementors/ContainedCommandImplementor.cs index bf0fcdb8b..316798885 100644 --- a/Projects/UOContent/Commands/Generic/Implementors/ContainedCommandImplementor.cs +++ b/Projects/UOContent/Commands/Generic/Implementors/ContainedCommandImplementor.cs @@ -67,7 +67,7 @@ namespace Server.Commands.Generic var list = new List(); - foreach (var item in cont.FindItemsByType()) + foreach (var item in cont.FindItemsByType()) { if (ext.IsValid(item)) { diff --git a/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs b/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs index f0cdb93dd..a0d476469 100644 --- a/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs +++ b/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs @@ -1737,23 +1737,20 @@ namespace Server.Engines.ConPVP var hadBomb = false; - corpse.FindItemsByType(false) - .ForEach( - bomb => - { - hadBomb = true; - bomb.DropTo(mob, killer); - } - ); + foreach (var bomb in corpse.EnumerateItemsByType(false)) + { + hadBomb = true; + bomb.DropTo(mob, killer); + } - mob.Backpack?.FindItemsByType(false) - .ForEach( - bomb => - { - hadBomb = true; - bomb.DropTo(mob, killer); - } - ); + if (mob.Backpack != null) + { + foreach (var bomb in mob.Backpack.EnumerateItemsByType(false)) + { + hadBomb = true; + bomb.DropTo(mob, killer); + } + } if (killer?.Player == true) { diff --git a/Projects/UOContent/Engines/ConPVP/Games/CTF.cs b/Projects/UOContent/Engines/ConPVP/Games/CTF.cs index 81319801e..9f9d42b14 100644 --- a/Projects/UOContent/Engines/ConPVP/Games/CTF.cs +++ b/Projects/UOContent/Engines/ConPVP/Games/CTF.cs @@ -1018,23 +1018,20 @@ namespace Server.Engines.ConPVP var hadFlag = false; - corpse.FindItemsByType(false) - .ForEach( - flag => - { - hadFlag = true; - flag.DropTo(mob, killer); - } - ); + foreach (var flag in corpse.EnumerateItemsByType(false)) + { + hadFlag = true; + flag.DropTo(mob, killer); + } - mob.Backpack?.FindItemsByType(false) - .ForEach( - flag => - { - hadFlag = true; - flag.DropTo(mob, killer); - } - ); + if (mob.Backpack != null) + { + foreach (var flag in mob.Backpack.EnumerateItemsByType(false)) + { + hadFlag = true; + flag.DropTo(mob, killer); + } + } if (killer?.Player == true) { diff --git a/Projects/UOContent/Engines/Craft/Core/CraftGump.cs b/Projects/UOContent/Engines/Craft/Core/CraftGump.cs index bceff2b1c..7a1c8d815 100644 --- a/Projects/UOContent/Engines/Craft/Core/CraftGump.cs +++ b/Projects/UOContent/Engines/Craft/Core/CraftGump.cs @@ -141,11 +141,12 @@ namespace Server.Engines.Craft if (from.Backpack != null) { - var items = from.Backpack.FindItemsByType(resourceType); - - for (var i = 0; i < items.Count; ++i) + foreach (var item in from.Backpack.FindItemsByType()) { - resourceCount += items[i].Amount; + if (resourceType.IsInstanceOfType(item)) + { + resourceCount += item.Amount; + } } } @@ -185,11 +186,12 @@ namespace Server.Engines.Craft if (from.Backpack != null) { - var items = from.Backpack.FindItemsByType(resourceType); - - for (var i = 0; i < items.Count; ++i) + foreach (var item in from.Backpack.FindItemsByType()) { - resourceCount += items[i].Amount; + if (resourceType.IsInstanceOfType(item)) + { + resourceCount += item.Amount; + } } } @@ -263,11 +265,13 @@ namespace Server.Engines.Craft if (from.Backpack != null) { - var items = from.Backpack.FindItemsByType(subResource.ItemType); - - for (var j = 0; j < items.Count; ++j) + var type = subResource.ItemType; + foreach (var item in from.Backpack.FindItemsByType()) { - resourceCount += items[j].Amount; + if (type.IsInstanceOfType(item)) + { + resourceCount += item.Amount; + } } } diff --git a/Projects/UOContent/Engines/Craft/Core/CraftItem.cs b/Projects/UOContent/Engines/Craft/Core/CraftItem.cs index 06512f997..942a6d6c6 100644 --- a/Projects/UOContent/Engines/Craft/Core/CraftItem.cs +++ b/Projects/UOContent/Engines/Craft/Core/CraftItem.cs @@ -685,7 +685,14 @@ namespace Server.Engines.Craft if (NameNumber == 1041267) { // Runebooks are a special case, they need a blank recall rune - consumeExtra = ourPack.FindItemsByType().Find(rune => !rune.Marked); + foreach (var rune in ourPack.FindItemsByType()) + { + if (!rune.Marked) + { + consumeExtra = rune; + break; + } + } if (consumeExtra == null) { diff --git a/Projects/UOContent/Engines/Factions/Core/Faction.cs b/Projects/UOContent/Engines/Factions/Core/Faction.cs index 112f82cad..a5315cc4c 100644 --- a/Projects/UOContent/Engines/Factions/Core/Faction.cs +++ b/Projects/UOContent/Engines/Factions/Core/Faction.cs @@ -385,7 +385,14 @@ namespace Server.Factions // Ordinarily, through normal faction removal, this will never find any sigils. // Only with a leave delay less than the ReturnPeriod or a Faction Kick/Ban, will this ever do anything - mob.Backpack?.FindItemsByType().ForEach(sigil => sigil.ReturnHome()); + + if (mob.Backpack != null) + { + foreach (var sigil in mob.Backpack.EnumerateItemsByType()) + { + sigil.ReturnHome(); + } + } if (pl.RankIndex != -1) { @@ -1039,35 +1046,36 @@ namespace Server.Factions var killerState = PlayerState.Find(killer); var killerPack = killer?.Backpack; - victim.Backpack?.FindItemsByType() - .ForEach( - sigil => - { - if (killerState == null || killerPack == null) - { - sigil.ReturnHome(); - return; - } - if (killer?.GetDistanceToSqrt(victim) > 64) - { - sigil.ReturnHome(); - killer.SendLocalizedMessage(1042230); // The sigil has gone back to its home location. - } - else if (Sigil.ExistsOn(killer)) - { - sigil.ReturnHome(); - // The sigil has gone back to its home location because you already have a sigil. - killer?.SendLocalizedMessage(1010258); - } - else if (!killerPack.TryDropItem(killer, sigil, false)) - { - sigil.ReturnHome(); - // The sigil has gone home because your backpack is full. - killer?.SendLocalizedMessage(1010259); - } + if (victim.Backpack != null) + { + foreach (var sigil in victim.Backpack.EnumerateItemsByType()) + { + if (killerState == null || killerPack == null) + { + sigil.ReturnHome(); + continue; } - ); + + if (killer?.GetDistanceToSqrt(victim) > 64) + { + sigil.ReturnHome(); + killer.SendLocalizedMessage(1042230); // The sigil has gone back to its home location. + } + else if (Sigil.ExistsOn(killer)) + { + sigil.ReturnHome(); + // The sigil has gone back to its home location because you already have a sigil. + killer?.SendLocalizedMessage(1010258); + } + else if (!killerPack.TryDropItem(killer, sigil, false)) + { + sigil.ReturnHome(); + // The sigil has gone home because your backpack is full. + killer?.SendLocalizedMessage(1010259); + } + } + } if (killerState == null) { @@ -1227,7 +1235,13 @@ namespace Server.Factions private static void EventSink_Logout(Mobile m) { - m.Backpack?.FindItemsByType().ForEach(sigil => sigil.ReturnHome()); + if (m.Backpack != null) + { + foreach (var sigil in m.Backpack.EnumerateItemsByType()) + { + sigil.ReturnHome(); + } + } } private static void EventSink_Login(Mobile m) => CheckLeaveTimer(m); diff --git a/Projects/UOContent/Engines/Factions/Mobiles/Guards/BaseFactionGuard.cs b/Projects/UOContent/Engines/Factions/Mobiles/Guards/BaseFactionGuard.cs index 9ce69ddee..acb63a3c6 100644 --- a/Projects/UOContent/Engines/Factions/Mobiles/Guards/BaseFactionGuard.cs +++ b/Projects/UOContent/Engines/Factions/Mobiles/Guards/BaseFactionGuard.cs @@ -1,5 +1,4 @@ using System; -using ModernUO.Serialization; using Server.Factions.AI; using Server.Items; using Server.Mobiles; diff --git a/Projects/UOContent/Engines/Harvest/Fishing.cs b/Projects/UOContent/Engines/Harvest/Fishing.cs index 6d91538bf..e24bf8624 100644 --- a/Projects/UOContent/Engines/Harvest/Fishing.cs +++ b/Projects/UOContent/Engines/Harvest/Fishing.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Server.Engines.Quests; using Server.Engines.Quests.Collector; using Server.Items; @@ -46,6 +45,27 @@ namespace Server.Engines.Harvest 0x34B5, 0x35D5 }; + private static readonly int[] bodyParts = { + 0x1CDD, 0x1CE5, // arm + 0x1CE0, 0x1CE8, // torso + 0x1CE1, 0x1CE9, // head + 0x1CE2, 0x1CEC // leg + }; + + private static readonly int[] boneParts = { + 0x1AE0, 0x1AE1, 0x1AE2, 0x1AE3, 0x1AE4, // skulls + 0x1B09, 0x1B0A, 0x1B0B, 0x1B0C, 0x1B0D, 0x1B0E, 0x1B0F, 0x1B10, // bone piles + 0x1B15, 0x1B16 // pelvis bones + }; + + private static readonly int[] miscItems = { + 0x1EB5, // unfinished barrel + 0xA2A, // stool + 0xC1F, // broken clock + 0x1047, 0x1048, // globe + 0x1EB1, 0x1EB2, 0x1EB3, 0x1EB4 // barrel staves + }; + private Fishing() { var fish = new HarvestDefinition @@ -101,7 +121,7 @@ namespace Server.Engines.Harvest Definitions = new[] { fish }; } - public static Fishing System => _system ?? (_system = new Fishing()); + public static Fishing System => _system ??= new Fishing(); public override void OnConcurrentHarvest(Mobile from, Item tool, HarvestDefinition def, object toHarvest) { @@ -179,12 +199,8 @@ namespace Server.Engines.Harvest if (pack != null) { - List messages = pack.FindItemsByType(); - - for (int i = 0; i < messages.Count; ++i) + foreach (var sos in pack.FindItemsByType()) { - SOS sos = messages[i]; - if ((from.Map == Map.Felucca || from.Map == Map.Trammel) && from.InRange(sos.TargetLocation, 60)) { return true; @@ -222,12 +238,9 @@ namespace Server.Engines.Harvest if (pack != null) { - var messages = pack.FindItemsByType(); - - for (var i = 0; i < messages.Count; ++i) + // We don't have to queue since we are returning on the first SOS. + foreach (var sos in pack.FindItemsByType()) { - var sos = messages[i]; - if ((from.Map == Map.Felucca || from.Map == Map.Trammel) && from.InRange(sos.TargetLocation, 60)) { Item preLoot = null; @@ -236,27 +249,12 @@ namespace Server.Engines.Harvest { case 0: // Body parts { - int[] list = - { - 0x1CDD, 0x1CE5, // arm - 0x1CE0, 0x1CE8, // torso - 0x1CE1, 0x1CE9, // head - 0x1CE2, 0x1CEC // leg - }; - - preLoot = new ShipwreckedItem(list.RandomElement()); + preLoot = new ShipwreckedItem(bodyParts.RandomElement()); break; } case 1: // Bone parts { - int[] list = - { - 0x1AE0, 0x1AE1, 0x1AE2, 0x1AE3, 0x1AE4, // skulls - 0x1B09, 0x1B0A, 0x1B0B, 0x1B0C, 0x1B0D, 0x1B0E, 0x1B0F, 0x1B10, // bone piles - 0x1B15, 0x1B16 // pelvis bones - }; - - preLoot = new ShipwreckedItem(list.RandomElement()); + preLoot = new ShipwreckedItem(boneParts.RandomElement()); break; } case 2: // Paintings and portraits @@ -276,36 +274,16 @@ namespace Server.Engines.Harvest } case 5: // Hats { - if (Utility.RandomBool()) - { - preLoot = new SkullCap(); - } - else - { - preLoot = new TricorneHat(); - } - + preLoot = Utility.RandomBool() ? new SkullCap() : new TricorneHat(); break; } case 6: // Misc { - int[] list = - { - 0x1EB5, // unfinished barrel - 0xA2A, // stool - 0xC1F, // broken clock - 0x1047, 0x1048, // globe - 0x1EB1, 0x1EB2, 0x1EB3, 0x1EB4 // barrel staves - }; + var rand = Utility.Random(miscItems.Length + 1); - if (Utility.Random(list.Length + 1) == 0) - { - preLoot = new Candelabra(); - } - else - { - preLoot = new ShipwreckedItem(list.RandomElement()); - } + preLoot = rand == miscItems.Length + ? new Candelabra() + : new ShipwreckedItem(miscItems[rand]); break; } @@ -317,16 +295,7 @@ namespace Server.Engines.Harvest return preLoot; } - LockableContainer chest; - - if (Utility.RandomBool()) - { - chest = new MetalGoldenChest(); - } - else - { - chest = new WoodenChest(); - } + LockableContainer chest = Utility.RandomBool() ? new MetalGoldenChest() : new WoodenChest(); if (sos.IsAncient) { diff --git a/Projects/UOContent/Engines/ML Quests/Objectives/CollectObjective.cs b/Projects/UOContent/Engines/ML Quests/Objectives/CollectObjective.cs index 0ea76721b..409fb9518 100644 --- a/Projects/UOContent/Engines/ML Quests/Objectives/CollectObjective.cs +++ b/Projects/UOContent/Engines/ML Quests/Objectives/CollectObjective.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using Server.Gumps; namespace Server.Engines.MLQuests.Objectives @@ -110,8 +109,16 @@ namespace Server.Engines.MLQuests.Objectives return 0; } - var items = pack.FindItemsByType(Objective.AcceptedType, false); // Note: subclasses are included - return items.Where(item => item.QuestItem && Objective.CheckItem(item)).Sum(item => item.Amount); + var total = 0; + foreach (var item in pack.FindItemsByType(false)) + { + if (ClaimTypePredicate(item) && item.QuestItem && Objective.CheckItem(item)) + { + total += item.Amount; + } + } + + return total; } public override bool AllowsQuestItem(Item item, Type type) => Objective.CheckType(type) && Objective.CheckItem(item); @@ -128,19 +135,20 @@ namespace Server.Engines.MLQuests.Objectives return; } - var checkType = Objective.AcceptedType; - var items = pack.FindItemsByType(checkType, false); - - foreach (var item in items) + foreach (var item in pack.FindItemsByType(false)) { - if (item.QuestItem && !MLQuestSystem.CanMarkQuestItem(pm, item, checkType) - ) // does another quest still need this item? (OSI just unmarks everything) + // does another quest still need this item? (OSI just unmarks everything) + if (ClaimTypePredicate(item) && + item.QuestItem && !MLQuestSystem.CanMarkQuestItem(pm, item, Objective.AcceptedType)) { item.QuestItem = false; } } } + // Note: subclasses are included + private bool ClaimTypePredicate(Item item) => Objective.AcceptedType.IsInstanceOfType(item); + // Should only be called after IsComplete() is checked to be true public override void OnClaimReward() { @@ -153,10 +161,9 @@ namespace Server.Engines.MLQuests.Objectives // TODO: OSI also counts the item in the cursor? - var items = pack.FindItemsByType(Objective.AcceptedType, false); var left = Objective.DesiredAmount; - foreach (var item in items) + foreach (var item in pack.EnumerateItemsByType(false, ClaimTypePredicate)) { if (item.QuestItem && Objective.CheckItem(item)) { diff --git a/Projects/UOContent/Engines/ML Quests/Objectives/DeliverObjective.cs b/Projects/UOContent/Engines/ML Quests/Objectives/DeliverObjective.cs index 87344e480..677655d1d 100644 --- a/Projects/UOContent/Engines/ML Quests/Objectives/DeliverObjective.cs +++ b/Projects/UOContent/Engines/ML Quests/Objectives/DeliverObjective.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Server.Gumps; using Server.Items; using Server.Logging; @@ -150,8 +149,16 @@ namespace Server.Engines.MLQuests.Objectives return 0; } - var items = pack.FindItemsByType(Objective.Delivery, false); // Note: subclasses are included - return items.Sum(item => item.Amount); + var total = 0; + foreach (var item in pack.FindItemsByType(false)) + { + if (ClaimTypePredicate(item)) + { + total += item.Amount; + } + } + + return total; } public override bool OnBeforeClaimReward() @@ -171,6 +178,9 @@ namespace Server.Engines.MLQuests.Objectives return true; } + // Note: subclasses are included + private bool ClaimTypePredicate(Item item) => Objective.Delivery.IsInstanceOfType(item); + // TODO: This is VERY similar to CollectObjective.OnClaimReward public override void OnClaimReward() { @@ -181,10 +191,9 @@ namespace Server.Engines.MLQuests.Objectives return; } - var items = pack.FindItemsByType(Objective.Delivery, false); var left = Objective.Amount; - foreach (var item in items) + foreach (var item in pack.EnumerateItemsByType(false, ClaimTypePredicate)) { if (left == 0) { diff --git a/Projects/UOContent/Engines/Plants/MainPlantGump.cs b/Projects/UOContent/Engines/Plants/MainPlantGump.cs index 810db2c73..d3d920531 100644 --- a/Projects/UOContent/Engines/Plants/MainPlantGump.cs +++ b/Projects/UOContent/Engines/Plants/MainPlantGump.cs @@ -327,11 +327,16 @@ namespace Server.Engines.Plants } case 6: // Water { - var bev = from.Backpack.FindItemsByType() - .Find( - beverage => - beverage.IsEmpty && beverage.Pourable && beverage.Content == BeverageType.Water - ); + BaseBeverage bev = null; + + foreach (var beverage in from.Backpack.FindItemsByType()) + { + if (beverage.IsEmpty && beverage.Pourable && beverage.Content == BeverageType.Water) + { + bev = beverage; + break; + } + } if (bev == null) { diff --git a/Projects/UOContent/Engines/Plants/PlantSystem.cs b/Projects/UOContent/Engines/Plants/PlantSystem.cs index 719ca1e93..80237d03e 100644 --- a/Projects/UOContent/Engines/Plants/PlantSystem.cs +++ b/Projects/UOContent/Engines/Plants/PlantSystem.cs @@ -1,5 +1,6 @@ using System; using ModernUO.Serialization; +using Server.Items; using Server.Misc; namespace Server.Engines.Plants @@ -402,28 +403,29 @@ namespace Server.Engines.Plants private static void EventSink_Login(Mobile from) { - from.Backpack?.FindItemsByType() - .ForEach( - plant => + Container cont = from.Backpack; + if (cont != null) + { + foreach (var plant in cont.FindItemsByType()) + { + if (plant.IsGrowable) { - if (plant.IsGrowable) - { - plant.PlantSystem.DoGrowthCheck(); - } + plant.PlantSystem.DoGrowthCheck(); } - ); + } + } - from.FindBankNoCreate() - ?.FindItemsByType() - .ForEach( - plant => + cont = from.FindBankNoCreate(); + if (cont != null) + { + foreach (var plant in cont.FindItemsByType()) + { + if (plant.IsGrowable) { - if (plant.IsGrowable) - { - plant.PlantSystem.DoGrowthCheck(); - } + plant.PlantSystem.DoGrowthCheck(); } - ); + } + } } public static void GrowAll() diff --git a/Projects/UOContent/Items/Aquarium/Aquarium.cs b/Projects/UOContent/Items/Aquarium/Aquarium.cs index acfe7bbb1..4b6351897 100644 --- a/Projects/UOContent/Items/Aquarium/Aquarium.cs +++ b/Projects/UOContent/Items/Aquarium/Aquarium.cs @@ -921,7 +921,20 @@ namespace Server.Items public static FishBowl GetEmptyBowl(Mobile from) { - return from?.Backpack?.FindItemsByType().Find(bowl => bowl.Empty); + if (from.Backpack == null) + { + return null; + } + + foreach (var bowl in from.Backpack.FindItemsByType()) + { + if (bowl.Empty) + { + return bowl; + } + } + + return null; } public static bool Accepts(Item item) diff --git a/Projects/UOContent/Items/Containers/SalvageBag.cs b/Projects/UOContent/Items/Containers/SalvageBag.cs index c6227402e..949227d0d 100644 --- a/Projects/UOContent/Items/Containers/SalvageBag.cs +++ b/Projects/UOContent/Items/Containers/SalvageBag.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using ModernUO.Serialization; using Server.ContextMenus; using Server.Engines.Craft; @@ -173,7 +172,20 @@ public partial class SalvageBag : Bag private void SalvageIngots(Mobile from) { - if (from.Backpack.FindItemsByType().All(tool => tool.CraftSystem != DefBlacksmithy.CraftSystem)) + var hasTool = false; + if (from.Backpack != null) + { + foreach (var tool in from.Backpack.FindItemsByType()) + { + if (tool.CraftSystem == DefBlacksmithy.CraftSystem) + { + hasTool = true; + break; + } + } + } + + if (!hasTool) { from.SendLocalizedMessage(1079822); // You need a blacksmithing tool in order to salvage ingots. return; @@ -190,11 +202,7 @@ public partial class SalvageBag : Bag var salvaged = 0; var notSalvaged = 0; - Container sBag = this; - - var smeltables = sBag.FindItemsByType(); - - foreach (var item in smeltables) + foreach (var item in FindItemsByType()) { if (item?.Deleted != false) { @@ -220,10 +228,8 @@ public partial class SalvageBag : Bag } else { - from.SendLocalizedMessage( - 1079973, - $"{salvaged}\t{salvaged + notSalvaged}" - ); // Salvaged: ~1_COUNT~/~2_NUM~ blacksmithed items + // Salvaged: ~1_COUNT~/~2_NUM~ blacksmithed items + from.SendLocalizedMessage(1079973, $"{salvaged}\t{salvaged + notSalvaged}"); } } @@ -245,14 +251,8 @@ public partial class SalvageBag : Bag var salvaged = 0; var notSalvaged = 0; - Container sBag = this; - - var scissorables = sBag.FindItemsByType(); - - for (var i = scissorables.Count - 1; i >= 0; --i) + foreach (var item in EnumerateItemsByType()) { - var item = scissorables[i]; - if (item is not IScissorable scissorable) { continue; diff --git a/Projects/UOContent/Items/Food/Beverage.cs b/Projects/UOContent/Items/Food/Beverage.cs index 750067f99..f71e3bb50 100644 --- a/Projects/UOContent/Items/Food/Beverage.cs +++ b/Projects/UOContent/Items/Food/Beverage.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using ModernUO.Serialization; +using Server.Collections; using Server.Engines.Plants; using Server.Engines.Quests; using Server.Engines.Quests.Hag; @@ -662,15 +663,15 @@ public abstract partial class BaseBeverage : Item, IHasQuantity public static bool ConsumeTotal(Container pack, Type itemType, BeverageType content, int quantity) { - var items = pack.FindItemsByType(itemType); + var total = 0; + using var queue = PooledRefQueue.Create(); // First pass, compute total - var total = 0; - - for (var i = 0; i < items.Count; ++i) + foreach (var bev in pack.FindItemsByType()) { - if (items[i] is BaseBeverage bev && bev.Content == content && !bev.IsEmpty) + if (itemType.IsInstanceOfType(bev) && bev.Content == content && !bev.IsEmpty) { + queue.Enqueue(bev); total += bev.Quantity; } } @@ -681,12 +682,9 @@ public abstract partial class BaseBeverage : Item, IHasQuantity var need = quantity; - for (var i = 0; i < items.Count; ++i) + while (queue.Count > 0) { - if (items[i] is not BaseBeverage bev || bev.Content != content || bev.IsEmpty) - { - continue; - } + var bev = queue.Dequeue(); var theirQuantity = bev.Quantity; diff --git a/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs b/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs index aca343865..9aba4ab72 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs @@ -83,12 +83,8 @@ public abstract partial class BasePotion : Item, ICraftable, ICommodity return 1; } - var kegs = pack.FindItemsByType(); - - for (var i = 0; i < kegs.Count; ++i) + foreach (var keg in pack.EnumerateItemsByType()) { - var keg = kegs[i]; - if (keg.Held is <= 0 or >= 100) { continue; diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index fd89c142a..bde315b6d 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -2393,9 +2393,14 @@ namespace Server.Mobiles DropHolding(); + // During AOS+, insured/blessed items are moved out of their child containers and put directly into the backpack. + // This fixes a "bug" where players put blessed items in nested bags and they were dropped on death if (Core.AOS && Backpack?.Deleted == false) { - Backpack.FindItemsByType(FindItems_Callback).ForEach(item => Backpack.AddItem(item)); + foreach (var item in Backpack.EnumerateItemsByType(predicate: FindItems_Callback)) + { + Backpack.AddItem(item); + } } EquipSnapshot = new List(Items); @@ -3910,13 +3915,13 @@ namespace Server.Mobiles return; } - var items = new List(); + using var queue = PooledRefQueue.Create(128); foreach (var item in Items) { if (DisplayInItemInsuranceGump(item)) { - items.Add(item); + queue.Enqueue(item); } } @@ -3924,20 +3929,26 @@ namespace Server.Mobiles if (pack != null) { - items.AddRange(pack.FindItemsByType(DisplayInItemInsuranceGump)); + foreach (var item in pack.FindItemsByType()) + { + if (DisplayInItemInsuranceGump(item)) + { + queue.Enqueue(item); + } + } } // TODO: Investigate item sorting CloseGump(); - if (items.Count == 0) + if (queue.Count == 0) { SendLocalizedMessage(1114915, "", 0x35); // None of your current items meet the requirements for insurance. } else { - SendGump(new ItemInsuranceMenuGump(this, items.ToArray())); + SendGump(new ItemInsuranceMenuGump(this, queue.ToArray())); } } diff --git a/Projects/UOContent/Mobiles/Townfolk/Banker.cs b/Projects/UOContent/Mobiles/Townfolk/Banker.cs index 08f6eec55..627d0a9a1 100644 --- a/Projects/UOContent/Mobiles/Townfolk/Banker.cs +++ b/Projects/UOContent/Mobiles/Townfolk/Banker.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Server.Accounting; using Server.ContextMenus; using Server.Items; @@ -47,32 +46,36 @@ namespace Server.Mobiles if (bank != null) { - var gold = bank.FindItemsByType(); - var checks = bank.FindItemsByType(); + foreach (var gold in bank.FindItemsByType()) + { + balance += gold.Amount; + } - balance += gold.Aggregate(0L, (c, t) => c + t.Amount); if (balance >= int.MaxValue) { return int.MaxValue; } - balance += checks.Aggregate(0L, (c, t) => c + t.Worth); + foreach (var check in bank.FindItemsByType()) + { + balance += check.Worth; + } } - return Math.Max(0, (int)Math.Min(int.MaxValue, balance)); + return (int)Math.Clamp(balance, 0, int.MaxValue); } public static int GetBalance(Mobile m, out List gold, out List checks) { long balance = 0; + gold = checks = new List(); if (AccountGold.Enabled && m.Account != null) { balance = m.Account.GetTotalGold(); - if (balance > int.MaxValue) + if (balance >= int.MaxValue) { - gold = checks = new List(); return int.MaxValue; } } @@ -81,23 +84,25 @@ namespace Server.Mobiles if (bank != null) { - gold = bank.FindItemsByType(typeof(Gold)); - checks = bank.FindItemsByType(typeof(BankCheck)); + foreach (var g in bank.FindItemsByType()) + { + balance += g.Amount; + gold.Add(g); + } - balance += gold.OfType().Aggregate(0L, (c, t) => c + t.Amount); if (balance >= int.MaxValue) { return int.MaxValue; } - balance += checks.OfType().Aggregate(0L, (c, t) => c + t.Worth); - } - else - { - gold = checks = new List(); + foreach (var bc in bank.FindItemsByType()) + { + balance += bc.Worth; + checks.Add(bc); + } } - return Math.Max(0, (int)Math.Min(int.MaxValue, balance)); + return (int)Math.Clamp(balance, 0, int.MaxValue); } public static bool Withdraw(Mobile from, int amount) diff --git a/Projects/UOContent/Spells/Seventh/GateTravel.cs b/Projects/UOContent/Spells/Seventh/GateTravel.cs index e00846202..b53dc5f0f 100644 --- a/Projects/UOContent/Spells/Seventh/GateTravel.cs +++ b/Projects/UOContent/Spells/Seventh/GateTravel.cs @@ -4,7 +4,6 @@ using Server.Factions; using Server.Items; using Server.Misc; using Server.Mobiles; -using Server.Multis; namespace Server.Spells.Seventh {