From c552f6567327d8c29a7d359217d5f87e93305d66 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 25 Apr 2026 13:40:21 -0700 Subject: [PATCH] perf: Eliminates allocations in Container searching. (#2409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Removes per-call heap allocations from `Container`'s consume / find / group hot paths and from `BaseCreature.OnDeath`'s fame/karma tracking. The headline wins: kill the `List>` + `Item[][]` + `int[]` grouping bridges in `ConsumeTotal*` / `ConsumeTotalGrouped*` / `GetBestGroupAmount*`, and kill the per-call `Predicate` allocations in `FindItemsByType(Type)` / `FindItemsByType(Type[])`. ### `Container.cs` - `ConsumeTotal`, `ConsumeTotalGrouped`, `GetBestGroupAmount` now share four streaming helpers (`HasAmount`, `TryFindGroupMeetingAmount`, `BestGroupTotal`, `ConsumeSlice`) backed by `PooledRefList` instead of allocating per-group lists and jagged arrays. Two-phase validate-then-consume pattern preserved — all-or-nothing semantics for spell reagents, vendor pay, and crafting still hold. - `(Type)` / `(Type[])` / `(Type[][])` overload trios collapsed to single `ReadOnlySpan` + `ReadOnlySpan` implementations. Implicit `T[] → ReadOnlySpan` conversion means UOContent callers compile unchanged. - Unused overloads deleted: `ConsumeTotalGrouped(Type)`, `ConsumeTotalGrouped(Type[][])`, `GetBestGroupAmount(Type)`, `GetBestGroupAmount(Type[][])`, plus the never-called `TryDropItems` hook and its private `ItemStackEntry` struct. - Fixes a `PooledRefList` leak in `GetBestGroupAmount(Type[], …)` (missing `using`). - `m_ContainerData` / `m_Items` / `m_TotalGold` / `m_TotalItems` / `m_TotalWeight` / `ContainerData.m_Table` / `ContainerData.logger` renamed to the underscored convention. `m_Items` cross-file rename for the Container-side references in `Item.cs`; `Item.CompactInfo.m_Items` deliberately left alone (separate effort). - `CheckHold` parent walk simplified; trivial dispatch methods (`CheckHold` overloads, `OnItemAdded`, `OnItemRemoved`, `OnStackAttempt`) get `[MethodImpl(AggressiveInlining)]`; `Destroy` and `DisplayTo` cache `Items` outside the loop; dead comments removed. ### `Item.Enumerable.cs` - `FindItemsByType(Type)` previously allocated a `Predicate` per call (method-group conversion). `FindItemsByType(Type[])` allocated a closure capturing `types`. Both now construct the enumerator with a `Type` / `ReadOnlySpan` field directly, no delegate. - `FindItemsByTypeEnumerator` gains two constructors plus a `Matches(T)` helper that picks the right filter inline. Constructor chaining via a private 2-arg seed constructor incidentally fixes a pre-existing bug where `PooledRefQueue` was always rented at capacity 0 because `_recurse` hadn't been assigned yet. - `(Type[])` overload of `FindItemsByType` becomes `(ReadOnlySpan)`. - `EnumerateItemsByType(Type)` / `EnumerateItemsByType(ReadOnlySpan)` / `ListItemsByType(Type)` / `ListItemsByType(ReadOnlySpan)` simplified to delegate to the new alloc-free overloads instead of filtering manually. ### `Utility.cs` - `InTypeList(this T, Type[])` and `InTypeList(this Type, Type[])` switched to `ReadOnlySpan`. ### `BaseCreature.cs` - `OnDeath` per-death `List` / `List` / `List` for fame/karma tracking switched to `PooledRefList`. --- .../Tests/Items/ContainerTests.cs | 480 +++++++++ Projects/Server/Items/Container.cs | 972 ++++-------------- Projects/Server/Items/Item.Enumerable.cs | 88 +- Projects/Server/Items/Item.cs | 10 +- Projects/Server/Utilities/Utility.cs | 4 +- Projects/UOContent/Mobiles/BaseCreature.cs | 9 +- 6 files changed, 771 insertions(+), 792 deletions(-) diff --git a/Projects/Server.Tests/Tests/Items/ContainerTests.cs b/Projects/Server.Tests/Tests/Items/ContainerTests.cs index 5079069cc..6b7634da8 100644 --- a/Projects/Server.Tests/Tests/Items/ContainerTests.cs +++ b/Projects/Server.Tests/Tests/Items/ContainerTests.cs @@ -132,4 +132,484 @@ public class ContainerTests item => Assert.Equal(item3, item) ); } + + // ------------------------------------------------------------------------- + // ConsumeTotal / ConsumeTotalGrouped / GetBestGroupAmount / GetAmount / + // FindItemByType / ConsumeUpTo behavior locks. + // + // These tests pin down the public contract before the optimization pass: + // - all-or-nothing semantics across multi-slot consume + // - callback ordering and per-item delta values + // - "first sufficient group wins" (not best) for ConsumeTotalGrouped + // - grouper(a, b) receives the group leader as `a` + // - >= amount threshold for groups + // ------------------------------------------------------------------------- + + private static Container MakeContainer(uint serial = 0x100) => new((Serial)serial); + + private static Item MakeStack(uint serial, int amount, int hue = 0) + { + var item = new Item((Serial)serial) { Amount = amount }; + if (hue != 0) + { + item.Hue = hue; + } + return item; + } + + private static TestReagentA MakeReagentA(uint serial, int amount, int hue = 0) + { + var item = new TestReagentA((Serial)serial) { Amount = amount }; + if (hue != 0) + { + item.Hue = hue; + } + return item; + } + + private static TestReagentB MakeReagentB(uint serial, int amount, int hue = 0) + { + var item = new TestReagentB((Serial)serial) { Amount = amount }; + if (hue != 0) + { + item.Hue = hue; + } + return item; + } + + // Mirrors CraftItem.CheckHueGrouping: groups items that share a hue. + private static int HueGrouper(Item a, Item b) => b.Hue.CompareTo(a.Hue); + + [Fact] + public void TestConsumeTotal_SingleType_ExactAmount_Succeeds() + { + var c = MakeContainer(0x200); + c.AddItem(MakeReagentA(0x201, 5)); + + Assert.True(c.ConsumeTotal(typeof(TestReagentA), 5)); + Assert.Empty(c.Items); // Stack was fully consumed -> Delete() + } + + [Fact] + public void TestConsumeTotal_SingleType_AcrossStacks_PartialOnLast() + { + var c = MakeContainer(0x210); + var a = MakeReagentA(0x211, 3); + var b = MakeReagentA(0x212, 4); + c.AddItem(a); + c.AddItem(b); + + Assert.True(c.ConsumeTotal(typeof(TestReagentA), 5)); + // BFS order: a fully consumed (3), b partially (2 of 4 remain). + Assert.True(a.Deleted); + Assert.False(b.Deleted); + Assert.Equal(2, b.Amount); + } + + [Fact] + public void TestConsumeTotal_SingleType_NotEnough_NoConsumption() + { + var c = MakeContainer(0x220); + var a = MakeReagentA(0x221, 2); + var b = MakeReagentA(0x222, 2); + c.AddItem(a); + c.AddItem(b); + + Assert.False(c.ConsumeTotal(typeof(TestReagentA), 10)); + // Must not partially consume. + Assert.Equal(2, a.Amount); + Assert.Equal(2, b.Amount); + } + + [Fact] + public void TestConsumeTotal_MultiType_FailsAtSlot1_Slot0Untouched() + { + var c = MakeContainer(0x230); + var ra = MakeReagentA(0x231, 5); + var rb = MakeReagentB(0x232, 1); // not enough for slot 1 + c.AddItem(ra); + c.AddItem(rb); + + var result = c.ConsumeTotal( + new[] { typeof(TestReagentA), typeof(TestReagentB) }, + new[] { 5, 5 } + ); + + Assert.Equal(1, result); + // Critical all-or-nothing invariant. + Assert.Equal(5, ra.Amount); + Assert.Equal(1, rb.Amount); + } + + [Fact] + public void TestConsumeTotal_NestedContainer_RecurseCountsChildItems() + { + var outer = MakeContainer(0x240); + var inner = MakeContainer(0x241); + outer.AddItem(inner); + inner.AddItem(MakeReagentA(0x242, 4)); + outer.AddItem(MakeReagentA(0x243, 1)); + + Assert.True(outer.ConsumeTotal(typeof(TestReagentA), 5)); + // Both reagents deleted; inner container itself persists in outer. + Assert.Single(outer.Items); + Assert.Empty(inner.Items); + } + + [Fact] + public void TestConsumeTotal_CallbackFires_PerItem_WithDelta() + { + var c = MakeContainer(0x250); + c.AddItem(MakeReagentA(0x251, 3)); + c.AddItem(MakeReagentA(0x252, 4)); + + var calls = new List<(int serial, int amount)>(); + Assert.True( + c.ConsumeTotal( + typeof(TestReagentA), 5, true, + (item, amt) => calls.Add(((int)item.Serial.Value, amt)) + ) + ); + + Assert.Equal(2, calls.Count); + Assert.Equal((0x251, 3), calls[0]); // full first stack + Assert.Equal((0x252, 2), calls[1]); // partial second stack + } + + [Fact] + public void TestConsumeTotalGrouped_HueGrouping_FirstSufficientGroupWins() + { + // Two hue groups: blue (sum=4, insufficient), red (sum=8, sufficient). + // First group meeting >= amount in BFS order wins. Blue is first; it's + // skipped because insufficient. Red wins. Verify red consumed, blue intact. + var c = MakeContainer(0x260); + var blue1 = MakeReagentA(0x261, 2, hue: 0x10); + var blue2 = MakeReagentA(0x262, 2, hue: 0x10); + var red1 = MakeReagentA(0x263, 5, hue: 0x20); + var red2 = MakeReagentA(0x264, 3, hue: 0x20); + c.AddItem(blue1); + c.AddItem(blue2); + c.AddItem(red1); + c.AddItem(red2); + + var result = c.ConsumeTotalGrouped( + new[] { typeof(TestReagentA) }, new[] { 6 }, + true, null, HueGrouper + ); + + Assert.Equal(-1, result); + Assert.Equal(2, blue1.Amount); + Assert.Equal(2, blue2.Amount); + // Red consumed: 5 + 1 = 6 needed. + Assert.True(red1.Deleted); + Assert.False(red2.Deleted); + Assert.Equal(2, red2.Amount); + } + + [Fact] + public void TestConsumeTotalGrouped_GrouperReceivesGroupLeader() + { + // grouper(a, b) must receive the group's first item as `a`, not the + // previous item. This matters when grouping is order-sensitive. + var c = MakeContainer(0x270); + var i1 = MakeReagentA(0x271, 1, hue: 0x10); + var i2 = MakeReagentA(0x272, 1, hue: 0x10); + var i3 = MakeReagentA(0x273, 1, hue: 0x10); + c.AddItem(i1); + c.AddItem(i2); + c.AddItem(i3); + + var leaderSerials = new List(); + c.ConsumeTotalGrouped( + new[] { typeof(TestReagentA) }, new[] { 3 }, + true, null, + (a, b) => + { + leaderSerials.Add((int)a.Serial.Value); + return b.Hue.CompareTo(a.Hue); + } + ); + + // Leader for every comparison in the single group must be i1 (the first). + Assert.NotEmpty(leaderSerials); + Assert.All(leaderSerials, s => Assert.Equal((int)i1.Serial.Value, s)); + } + + [Fact] + public void TestConsumeTotalGrouped_PartialFailure_NothingConsumedFromAnySlot() + { + // Slot 0 has enough; slot 1 does not. Nothing consumed, return 1. + var c = MakeContainer(0x280); + var a = MakeReagentA(0x281, 5, hue: 0x10); + var b = MakeReagentB(0x282, 1, hue: 0x10); + c.AddItem(a); + c.AddItem(b); + + var result = c.ConsumeTotalGrouped( + new[] { typeof(TestReagentA), typeof(TestReagentB) }, + new[] { 5, 5 }, + true, null, HueGrouper + ); + + Assert.Equal(1, result); + Assert.Equal(5, a.Amount); + Assert.Equal(1, b.Amount); + } + + [Fact] + public void TestConsumeTotalGrouped_ExactGroupAmount_Succeeds() + { + // Group sum equals exactly the requested amount. Threshold is >=, so + // a group of exactly N satisfies a request for N. + var c = MakeContainer(0x330); + var stack = MakeReagentA(0x331, 5, hue: 0x10); + c.AddItem(stack); + + var result = c.ConsumeTotalGrouped( + new[] { typeof(TestReagentA) }, new[] { 5 }, + true, null, HueGrouper + ); + + Assert.Equal(-1, result); + Assert.True(stack.Deleted); + } + + [Fact] + public void TestConsumeTotalGrouped_CallbackOrderMatchesBFS() + { + // OnResourceConsumed in CraftItem.cs retains the hue of the largest + // consumed stack, so callback order must match BFS iteration order. + var c = MakeContainer(0x290); + c.AddItem(MakeReagentA(0x291, 3, hue: 0x10)); + c.AddItem(MakeReagentA(0x292, 4, hue: 0x10)); + + var calls = new List<(int serial, int amount)>(); + c.ConsumeTotalGrouped( + new[] { typeof(TestReagentA) }, new[] { 5 }, + true, + (item, amt) => calls.Add(((int)item.Serial.Value, amt)), + HueGrouper + ); + + Assert.Equal(2, calls.Count); + Assert.Equal((0x291, 3), calls[0]); + Assert.Equal((0x292, 2), calls[1]); + } + + [Fact] + public void TestGetBestGroupAmount_ReturnsLargestGroupSum() + { + var c = MakeContainer(0x2A0); + c.AddItem(MakeReagentA(0x2A1, 2, hue: 0x10)); + c.AddItem(MakeReagentA(0x2A2, 2, hue: 0x10)); + c.AddItem(MakeReagentA(0x2A3, 5, hue: 0x20)); + c.AddItem(MakeReagentA(0x2A4, 3, hue: 0x20)); + + var best = c.GetBestGroupAmount(new[] { typeof(TestReagentA) }, true, HueGrouper); + Assert.Equal(8, best); + } + + [Fact] + public void TestGetBestGroupAmount_EmptyOrNoMatch_ReturnsZero() + { + var c = MakeContainer(0x2B0); + Assert.Equal(0, c.GetBestGroupAmount(new[] { typeof(TestReagentA) }, true, HueGrouper)); + + c.AddItem(MakeReagentB(0x2B1, 5)); + Assert.Equal(0, c.GetBestGroupAmount(new[] { typeof(TestReagentA) }, true, HueGrouper)); + } + + [Fact] + public void TestGetBestGroupAmount_NullGrouper_Throws() + { + var c = MakeContainer(0x2C0); + Assert.Throws( + () => c.GetBestGroupAmount(new[] { typeof(TestReagentA) }, true, null) + ); + } + + [Fact] + public void TestGetAmount_TypeArray_SumsAcrossTypes() + { + var c = MakeContainer(0x2D0); + c.AddItem(MakeReagentA(0x2D1, 3)); + c.AddItem(MakeReagentB(0x2D2, 4)); + c.AddItem(MakeStack(0x2D3, 99)); // base Item, doesn't match A or B + + var total = c.GetAmount(new[] { typeof(TestReagentA), typeof(TestReagentB) }); + Assert.Equal(7, total); + } + + [Fact] + public void TestFindItemByType_Generic_WithPredicate() + { + var c = MakeContainer(0x2E0); + c.AddItem(MakeReagentA(0x2E1, 1, hue: 0x10)); + var target = MakeReagentA(0x2E2, 1, hue: 0x20); + c.AddItem(target); + c.AddItem(MakeReagentA(0x2E3, 1, hue: 0x10)); + + var found = c.FindItemByType(true, item => item.Hue == 0x20); + Assert.Same(target, found); + } + + [Fact] + public void TestConsumeUpTo_DeletesExhaustedStacks() + { + var c = MakeContainer(0x2F0); + var s1 = MakeReagentA(0x2F1, 3); + var s2 = MakeReagentA(0x2F2, 3); + c.AddItem(s1); + c.AddItem(s2); + + var consumed = c.ConsumeUpTo(typeof(TestReagentA), 5); + Assert.Equal(5, consumed); + // BFS: first stack fully (3), second partially (2 of 3 remain). + Assert.True(s1.Deleted); + Assert.False(s2.Deleted); + Assert.Equal(1, s2.Amount); + } + + [Fact] + public void TestConsumeTotal_LengthMismatch_Throws() + { + var c = MakeContainer(0x300); + Assert.Throws( + () => c.ConsumeTotal(new[] { typeof(TestReagentA) }, new[] { 1, 2 }) + ); + } + + [Fact] + public void TestConsumeTotalGrouped_LengthMismatch_Throws() + { + var c = MakeContainer(0x310); + Assert.Throws( + () => c.ConsumeTotalGrouped( + new[] { typeof(TestReagentA) }, new[] { 1, 2 }, + true, null, HueGrouper + ) + ); + } + + [Fact] + public void TestConsumeTotalGrouped_NullGrouper_Throws() + { + var c = MakeContainer(0x320); + Assert.Throws( + () => c.ConsumeTotalGrouped( + new[] { typeof(TestReagentA) }, new[] { 1 }, + true, null, null + ) + ); + } + + // ------------------------------------------------------------------------- + // FindItemsByType(Type) and FindItemsByType(ReadOnlySpan) — Phase 10: + // these used to allocate a Predicate per call (method-group conversion + // for the single-Type case, real closure for the Type[] case). The + // enumerator now stores the filter directly. + // ------------------------------------------------------------------------- + + [Fact] + public void TestFindItemsByType_RuntimeType_MatchesPredicatePath() + { + var c = MakeContainer(0x340); + c.AddItem(MakeReagentA(0x341, 1)); + c.AddItem(MakeReagentB(0x342, 1)); + c.AddItem(MakeReagentA(0x343, 1)); + c.AddItem(MakeStack(0x344, 1)); // base Item, doesn't match TestReagentA + + var fromRuntime = new List(); + foreach (var item in c.FindItemsByType(typeof(TestReagentA))) + { + fromRuntime.Add((int)item.Serial.Value); + } + + var fromGeneric = new List(); + foreach (var item in c.FindItemsByType()) + { + fromGeneric.Add((int)item.Serial.Value); + } + + Assert.Equal(fromGeneric, fromRuntime); + Assert.Equal(new[] { 0x341, 0x343 }, fromRuntime); + } + + [Fact] + public void TestFindItemsByType_TypeSpan_MatchesUnionOfTypes() + { + var c = MakeContainer(0x350); + c.AddItem(MakeReagentA(0x351, 1)); + c.AddItem(MakeReagentB(0x352, 1)); + c.AddItem(MakeStack(0x353, 1)); // base Item + + var serials = new List(); + foreach (var item in c.FindItemsByType(new[] { typeof(TestReagentA), typeof(TestReagentB) })) + { + serials.Add((int)item.Serial.Value); + } + + Assert.Equal(new[] { 0x351, 0x352 }, serials); + } + + [Fact] + public void TestFindItemsByType_RuntimeType_NoAllocations() + { + // Establishes that the (Type) path no longer allocates a Predicate per + // call. Snapshot allocation counter, run a few iterations, assert flat. + var c = MakeContainer(0x360); + c.AddItem(MakeReagentA(0x361, 1)); + c.AddItem(MakeReagentA(0x362, 1)); + + // Warm up to load any first-call jitting. + foreach (var _ in c.FindItemsByType(typeof(TestReagentA))) { } + + var before = GC.GetAllocatedBytesForCurrentThread(); + for (var i = 0; i < 100; i++) + { + foreach (var _ in c.FindItemsByType(typeof(TestReagentA)) ) { } + } + var delta = GC.GetAllocatedBytesForCurrentThread() - before; + + // PooledRefQueue rents from the pool but the rental itself doesn't + // allocate when the bucket is warm. Allow a small headroom for any + // first-rent-after-pool-cleanup allocations but well below the ~48 + // bytes/call the old delegate path would have produced (=4800 bytes). + Assert.True(delta < 1024, $"Expected near-zero allocations, got {delta} bytes across 100 iterations"); + } + + [Fact] + public void TestFindItemsByType_TypeSpan_NoAllocations() + { + var c = MakeContainer(0x370); + c.AddItem(MakeReagentA(0x371, 1)); + c.AddItem(MakeReagentB(0x372, 1)); + + var types = new[] { typeof(TestReagentA), typeof(TestReagentB) }; + foreach (var _ in c.FindItemsByType(types)) { } // warm + + var before = GC.GetAllocatedBytesForCurrentThread(); + for (var i = 0; i < 100; i++) + { + foreach (var _ in c.FindItemsByType(types)) { } + } + var delta = GC.GetAllocatedBytesForCurrentThread() - before; + + // Old closure path: ~80 bytes/call → ~8000 bytes for 100 iterations. + Assert.True(delta < 1024, $"Expected near-zero allocations, got {delta} bytes across 100 iterations"); + } +} + +// Test-only Item subclasses used to differentiate concrete types in the +// consume/find/group tests above. They have no serialization generator +// (the tests never round-trip through World). Stackable is set so the Amount +// setter doesn't log "Amount changed for non-stackable item" warnings. +public sealed class TestReagentA : Item +{ + public TestReagentA(Serial serial) : base(serial) => Stackable = true; +} + +public sealed class TestReagentB : Item +{ + public TestReagentB(Serial serial) : base(serial) => Stackable = true; } diff --git a/Projects/Server/Items/Container.cs b/Projects/Server/Items/Container.cs index 6ee943a98..34dbbbbb1 100644 --- a/Projects/Server/Items/Container.cs +++ b/Projects/Server/Items/Container.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.CompilerServices; using Server.Collections; using Server.Logging; using Server.Network; @@ -32,14 +33,14 @@ public delegate void ContainerSnoopHandler(Container cont, Mobile from); [SerializationGenerator(0, false)] public partial class Container : Item { - private ContainerData m_ContainerData; + private ContainerData _containerData; - internal List m_Items; + internal List _items; - private int m_TotalGold; + private int _totalGold; - private int m_TotalItems; - private int m_TotalWeight; + private int _totalItems; + private int _totalWeight; internal int _version; [SerializableField(3)] @@ -60,8 +61,8 @@ public partial class Container : Item public ContainerData ContainerData { - get => m_ContainerData ?? UpdateContainerData(); - set => m_ContainerData = value; + get => _containerData ?? UpdateContainerData(); + set => _containerData = value; } [CommandProperty(AccessLevel.GameMaster)] @@ -76,7 +77,7 @@ public partial class Container : Item if (ItemID != oldID) { - m_ContainerData = null; + _containerData = null; } } } @@ -201,8 +202,10 @@ public partial class Container : Item return base.CheckItemUse(from, item); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CheckHold(Mobile m, Item item, bool message) => CheckHold(m, item, message, true, 0, 0); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CheckHold(Mobile m, Item item, bool message, bool checkItems) => CheckHold(m, item, message, checkItems, 0, 0); @@ -246,18 +249,13 @@ public partial class Container : Item var parent = Parent; - while (parent != null) + while (parent is Item parentItem) { - if (parent is Container container) + if (parentItem is Container container) { return container.CheckHold(m, item, message, checkItems, plusItems, plusWeight); } - if (parent is not Item parentItem) - { - break; - } - parent = parentItem.Parent; } @@ -306,9 +304,9 @@ public partial class Container : Item { return type switch { - TotalType.Gold => m_TotalGold, - TotalType.Items => m_TotalItems, - TotalType.Weight => m_TotalWeight, + TotalType.Gold => _totalGold, + TotalType.Items => _totalItems, + TotalType.Weight => _totalWeight, _ => base.GetTotal(type) }; } @@ -321,20 +319,20 @@ public partial class Container : Item { case TotalType.Gold: { - m_TotalGold += delta; + _totalGold += delta; break; } case TotalType.Items: { - m_TotalItems += delta; + _totalItems += delta; InvalidateProperties(); break; } case TotalType.Weight: { - m_TotalWeight += delta; + _totalWeight += delta; InvalidateProperties(); break; } @@ -346,11 +344,11 @@ public partial class Container : Item public override void UpdateTotals() { - m_TotalGold = 0; - m_TotalItems = 0; - m_TotalWeight = 0; + _totalGold = 0; + _totalItems = 0; + _totalWeight = 0; - var items = m_Items; + var items = _items; if (items == null) { @@ -368,24 +366,27 @@ public partial class Container : Item continue; } - m_TotalGold += item.TotalGold; - m_TotalItems += item.TotalItems + 1; - m_TotalWeight += item.TotalWeight + item.PileWeight; + _totalGold += item.TotalGold; + _totalItems += item.TotalItems + 1; + _totalWeight += item.TotalWeight + item.PileWeight; } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void OnItemAdded(Item item) { base.OnItemAdded(item); _version++; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void OnItemRemoved(Item item) { base.OnItemRemoved(item); _version++; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual bool OnStackAttempt(Mobile from, Item stack, Item dropped) => CheckHold(from, dropped, true, false) && stack.StackWith(from, dropped); @@ -428,76 +429,17 @@ public partial class Container : Item return false; } - public virtual bool TryDropItems(Mobile from, bool sendFullMessage, params ReadOnlySpan droppedItems) - { - using var dropItems = PooledRefQueue.Create(); - using var stackItems = PooledRefQueue.Create(); - - var extraItems = 0; - var extraWeight = 0; - - for (var i = 0; i < droppedItems.Length; i++) - { - var dropped = droppedItems[i]; - - var list = Items; - - var stacked = false; - - for (var j = 0; j < list.Count; ++j) - { - var item = list[j]; - - if (item is not Container && CheckHold(from, dropped, false, false, 0, extraWeight) && - item.CanStackWith(dropped)) - { - stackItems.Enqueue(new ItemStackEntry(item, dropped)); - extraWeight += (int)Math.Ceiling(item.Weight * (item.Amount + dropped.Amount)) - - item.PileWeight; // extra weight delta, do not need TotalWeight as we do not have hybrid stackable container types - stacked = true; - break; - } - } - - if (!stacked && CheckHold(from, dropped, false, true, extraItems, extraWeight)) - { - dropItems.Enqueue(dropped); - extraItems++; - extraWeight += dropped.TotalWeight + dropped.PileWeight; - } - } - - if (dropItems.Count + stackItems.Count == droppedItems.Length) // All good - { - while (dropItems.Count > 0) - { - DropItem(dropItems.Dequeue()); - } - - while (stackItems.Count > 0) - { - var stackItem = stackItems.Dequeue(); - stackItem.m_StackItem.StackWith(from, stackItem.m_DropItem, false); - } - - return true; - } - - return false; - } - public virtual void Destroy() { var loc = GetWorldLocation(); var map = Map; + var items = Items; - for (var i = Items.Count - 1; i >= 0; --i) + for (var i = items.Count - 1; i >= 0; --i) { - if (i < Items.Count) - { - Items[i].SetLastMoved(); - Items[i].MoveToWorld(loc, map); - } + var item = items[i]; + item.SetLastMoved(); + item.MoveToWorld(loc, map); } Delete(); @@ -590,8 +532,6 @@ public partial class Container : Item LabelTo(from, $"({TotalItems} items, {TotalWeight} stones)"); } } - - // LabelTo( from, 1050044, String.Format( "{0}\t{1}", TotalItems.ToString(), TotalWeight.ToString() ) ); } public override void OnDelete() @@ -615,9 +555,10 @@ public partial class Container : Item if (ObjectPropertyList.Enabled) { - for (var i = 0; i < Items.Count; ++i) + var items = Items; + for (var i = 0; i < items.Count; ++i) { - ns.SendOPLInfo(Items[i]); + ns.SendOPLInfo(items[i]); } } } @@ -659,7 +600,7 @@ public partial class Container : Item if (!contains) { - Openers ??= new List(); + Openers ??= []; Openers.Add(opener); } @@ -716,219 +657,37 @@ public partial class Container : Item } } - public bool ConsumeTotalGrouped(Type type, int amount, bool recurse, OnItemConsumed callback, CheckItemGroup grouper) - { - if (grouper == null) - { - throw new ArgumentNullException(nameof(grouper)); - } - - using var typedItems = ListItemsByType(type, recurse); - - var groups = new List>(); - var idx = 0; - - while (idx < typedItems.Count) - { - var a = typedItems[idx++]; - var group = new List - { - a - }; - - while (idx < typedItems.Count) - { - var b = typedItems[idx]; - var v = grouper(a, b); - - if (v == 0) - { - group.Add(b); - } - else - { - break; - } - - ++idx; - } - - groups.Add(group); - } - - var items = new Item[groups.Count][]; - var totals = new int[groups.Count]; - - var hasEnough = false; - - for (var i = 0; i < groups.Count; ++i) - { - items[i] = groups[i].ToArray(); - - for (var j = 0; j < items[i].Length; ++j) - { - totals[i] += items[i][j].Amount; - } - - if (totals[i] >= amount) - { - hasEnough = true; - } - } - - if (!hasEnough) - { - return false; - } - - for (var i = 0; i < items.Length; ++i) - { - if (totals[i] >= amount) - { - var need = amount; - - for (var j = 0; j < items[i].Length; ++j) - { - var item = items[i][j]; - - var theirAmount = item.Amount; - - if (theirAmount < need) - { - callback?.Invoke(item, theirAmount); - - item.Consume(theirAmount); - need -= theirAmount; - } - else - { - callback?.Invoke(item, need); - - item.Consume(need); - break; - } - } - - break; - } - } - - return true; - } - public int ConsumeTotalGrouped( - Type[] types, int[] amounts, bool recurse, OnItemConsumed callback, - CheckItemGroup grouper - ) + ReadOnlySpan types, ReadOnlySpan amounts, + bool recurse, OnItemConsumed callback, CheckItemGroup grouper) { if (types.Length != amounts.Length) { throw new ArgumentException("length of types and amounts must match"); } + ArgumentNullException.ThrowIfNull(grouper); - if (grouper == null) + // Phase 1: every slot must have a single group with sum >= amount + // before any consume happens (preserves all-or-nothing semantics). + for (var i = 0; i < types.Length; i++) { - throw new ArgumentNullException(nameof(grouper)); - } - - var items = new Item[types.Length][][]; - var totals = new int[types.Length][]; - - for (var i = 0; i < types.Length; ++i) - { - var type = types[i]; - - using var typedItems = ListItemsByType(type, recurse); - - var groups = new List>(); - var idx = 0; - - while (idx < typedItems.Count) - { - var a = typedItems[idx++]; - var group = new List - { - a - }; - - while (idx < typedItems.Count) - { - var b = typedItems[idx]; - var v = grouper(a, b); - - if (v == 0) - { - group.Add(b); - } - else - { - break; - } - - ++idx; - } - - groups.Add(group); - } - - items[i] = new Item[groups.Count][]; - totals[i] = new int[groups.Count]; - - var hasEnough = false; - - for (var j = 0; j < groups.Count; ++j) - { - items[i][j] = groups[j].ToArray(); - - for (var k = 0; k < items[i][j].Length; ++k) - { - totals[i][j] += items[i][j][k].Amount; - } - - if (totals[i][j] >= amounts[i]) - { - hasEnough = true; - } - } - - if (!hasEnough) + using var items = ListItemsByType(types[i], recurse); + if (!TryFindGroupMeetingAmount(items, amounts[i], grouper, out _, out _)) { return i; } } - for (var i = 0; i < items.Length; ++i) + // Phase 2: re-list and consume. Trades one extra ListItemsByType per + // slot (small) for eliminating List> + Item[][] + int[] + // grouping bridges (large). Live mutation of the container during + // Item.Consume bumps _version, so each phase walks its own snapshot. + for (var i = 0; i < types.Length; i++) { - for (var j = 0; j < items[i].Length; ++j) + using var items = ListItemsByType(types[i], recurse); + if (TryFindGroupMeetingAmount(items, amounts[i], grouper, out var start, out var len)) { - if (totals[i][j] >= amounts[i]) - { - var need = amounts[i]; - - for (var k = 0; k < items[i][j].Length; ++k) - { - var item = items[i][j][k]; - - var theirAmount = item.Amount; - - if (theirAmount < need) - { - callback?.Invoke(item, theirAmount); - - item.Consume(theirAmount); - need -= theirAmount; - } - else - { - callback?.Invoke(item, need); - - item.Consume(need); - break; - } - } - - break; - } + ConsumeSlice(items, start, len, amounts[i], callback); } } @@ -936,8 +695,64 @@ public partial class Container : Item } public int ConsumeTotalGrouped( - Type[][] types, int[] amounts, bool recurse, OnItemConsumed callback, - CheckItemGroup grouper + Type[][] types, ReadOnlySpan amounts, + bool recurse, OnItemConsumed callback, CheckItemGroup grouper) + { + if (types.Length != amounts.Length) + { + throw new ArgumentException("length of types and amounts must match"); + } + ArgumentNullException.ThrowIfNull(grouper); + + for (var i = 0; i < types.Length; i++) + { + using var items = ListItemsByType(types[i], recurse); + if (!TryFindGroupMeetingAmount(items, amounts[i], grouper, out _, out _)) + { + return i; + } + } + + for (var i = 0; i < types.Length; i++) + { + using var items = ListItemsByType(types[i], recurse); + if (TryFindGroupMeetingAmount(items, amounts[i], grouper, out var start, out var len)) + { + ConsumeSlice(items, start, len, amounts[i], callback); + } + } + + return -1; + } + + public int ConsumeTotal(Type[][] types, ReadOnlySpan amounts, bool recurse = true, OnItemConsumed callback = null) + { + if (types.Length != amounts.Length) + { + throw new ArgumentException("length of types and amounts must match"); + } + + // Phase 1: validate every slot before any consume (all-or-nothing). + for (var i = 0; i < types.Length; i++) + { + if (GetAmount(types[i], recurse) < amounts[i]) + { + return i; + } + } + + // Phase 2: materialize per slot and consume. + for (var i = 0; i < types.Length; i++) + { + using var items = ListItemsByType(types[i], recurse); + ConsumeSlice(items, 0, items.Count, amounts[i], callback); + } + + return -1; + } + + public int ConsumeTotal( + ReadOnlySpan types, ReadOnlySpan amounts, bool recurse = true, OnItemConsumed callback = null ) { if (types.Length != amounts.Length) @@ -945,224 +760,18 @@ public partial class Container : Item throw new ArgumentException("length of types and amounts must match"); } - if (grouper == null) + for (var i = 0; i < types.Length; i++) { - throw new ArgumentNullException(nameof(grouper)); - } - - var items = new Item[types.Length][][]; - var totals = new int[types.Length][]; - - for (var i = 0; i < types.Length; ++i) - { - using var typedItems = ListItemsByType(types[i], recurse); - - var groups = new List>(); - var idx = 0; - - while (idx < typedItems.Count) - { - var a = typedItems[idx++]; - var group = new List - { - a - }; - - while (idx < typedItems.Count) - { - var b = typedItems[idx]; - var v = grouper(a, b); - - if (v == 0) - { - group.Add(b); - } - else - { - break; - } - - ++idx; - } - - groups.Add(group); - } - - items[i] = new Item[groups.Count][]; - totals[i] = new int[groups.Count]; - - var hasEnough = false; - - for (var j = 0; j < groups.Count; ++j) - { - items[i][j] = groups[j].ToArray(); - - for (var k = 0; k < items[i][j].Length; ++k) - { - totals[i][j] += items[i][j][k].Amount; - } - - if (totals[i][j] >= amounts[i]) - { - hasEnough = true; - } - } - - if (!hasEnough) + if (GetAmount(types[i], recurse) < amounts[i]) { return i; } } - for (var i = 0; i < items.Length; ++i) + for (var i = 0; i < types.Length; i++) { - for (var j = 0; j < items[i].Length; ++j) - { - if (totals[i][j] >= amounts[i]) - { - var need = amounts[i]; - - for (var k = 0; k < items[i][j].Length; ++k) - { - var item = items[i][j][k]; - - var theirAmount = item.Amount; - - if (theirAmount < need) - { - callback?.Invoke(item, theirAmount); - - item.Consume(theirAmount); - need -= theirAmount; - } - else - { - callback?.Invoke(item, need); - - item.Consume(need); - break; - } - } - - break; - } - } - } - - return -1; - } - - public int ConsumeTotal(Type[][] types, int[] amounts, bool recurse = true, OnItemConsumed callback = null) - { - if (types.Length != amounts.Length) - { - throw new ArgumentException("length of types and amounts must match"); - } - - var items = new Item[types.Length][]; - var totals = new int[types.Length]; - - for (var i = 0; i < types.Length; ++i) - { - using var typedItems = ListItemsByType(types[i], recurse); - - items[i] = new Item[typedItems.Count]; - - for (var j = 0; j < typedItems.Count; ++j) - { - items[i][j] = typedItems[j]; - totals[i] += typedItems[j].Amount; - } - - if (totals[i] < amounts[i]) - { - return i; - } - } - - for (var i = 0; i < types.Length; ++i) - { - var need = amounts[i]; - - for (var j = 0; j < items[i].Length; ++j) - { - var item = items[i][j]; - - var theirAmount = item.Amount; - - if (theirAmount < need) - { - callback?.Invoke(item, theirAmount); - - item.Consume(theirAmount); - need -= theirAmount; - } - else - { - callback?.Invoke(item, need); - - item.Consume(need); - break; - } - } - } - - return -1; - } - - public int ConsumeTotal(Type[] types, int[] amounts, bool recurse = true, OnItemConsumed callback = null) - { - if (types.Length != amounts.Length) - { - throw new ArgumentException("length of types and amounts must match"); - } - - var items = new Item[types.Length][]; - var totals = new int[types.Length]; - - for (var i = 0; i < types.Length; ++i) - { - using var typedItems = ListItemsByType(types[i], recurse); - - items[i] = new Item[typedItems.Count]; - - for (var j = 0; j < typedItems.Count; ++j) - { - items[i][j] = typedItems[j]; - totals[i] += typedItems[j].Amount; - } - - if (totals[i] < amounts[i]) - { - return i; - } - } - - for (var i = 0; i < types.Length; ++i) - { - var need = amounts[i]; - - for (var j = 0; j < items[i].Length; ++j) - { - var item = items[i][j]; - - var theirAmount = item.Amount; - - if (theirAmount < need) - { - callback?.Invoke(item, theirAmount); - - item.Consume(theirAmount); - need -= theirAmount; - } - else - { - callback?.Invoke(item, need); - - item.Consume(need); - break; - } - } + using var items = ListItemsByType(types[i], recurse); + ConsumeSlice(items, 0, items.Count, amounts[i], callback); } return -1; @@ -1170,48 +779,14 @@ public partial class Container : Item public bool ConsumeTotal(Type type, int amount = 1, bool recurse = true, OnItemConsumed callback = null) { - var total = 0; - - using var typedItems = ListItemsByType(type, recurse); - - // First pass, compute total - foreach (var item in typedItems) + if (!HasAmount(type, amount, recurse)) { - total += item.Amount; - - if (total >= amount) - { - break; - } + return false; } - // We have enough, so consume it - if (total >= amount) - { - var need = amount; - - foreach (var item in typedItems) - { - var theirAmount = item.Amount; - - if (theirAmount < need) - { - callback?.Invoke(item, theirAmount); - - item.Consume(theirAmount); - need -= theirAmount; - } - else - { - callback?.Invoke(item, need); - - item.Consume(need); - return true; - } - } - } - - return false; + using var items = ListItemsByType(type, recurse); + ConsumeSlice(items, 0, items.Count, amount, callback); + return true; } public int ConsumeUpTo(Type type, int amount, bool recurse = true) @@ -1233,8 +808,7 @@ public partial class Container : Item } private static void RecurseConsumeUpTo( - Item current, Type type, int amount, bool recurse, ref int consumed, - ref PooledRefQueue toDelete + Item current, Type type, int amount, bool recurse, ref int consumed, ref PooledRefQueue toDelete ) { if (current == null || current.Items.Count == 0) @@ -1273,191 +847,11 @@ public partial class Container : Item } } - public int GetBestGroupAmount(Type type, bool recurse, CheckItemGroup grouper) - { - if (grouper == null) - { - throw new ArgumentNullException(nameof(grouper)); - } - - var best = 0; - - using var typedItems = ListItemsByType(type, recurse); - - var groups = new List>(); - var idx = 0; - - while (idx < typedItems.Count) - { - var a = typedItems[idx++]; - var group = new List - { - a - }; - - while (idx < typedItems.Count) - { - var b = typedItems[idx]; - var v = grouper(a, b); - - if (v == 0) - { - group.Add(b); - } - else - { - break; - } - - ++idx; - } - - groups.Add(group); - } - - for (var i = 0; i < groups.Count; ++i) - { - var items = groups[i].ToArray(); - - var total = 0; - - for (var j = 0; j < items.Length; ++j) - { - total += items[j].Amount; - } - - if (total >= best) - { - best = total; - } - } - - return best; - } - public int GetBestGroupAmount(Type[] types, bool recurse, CheckItemGroup grouper) { - if (grouper == null) - { - throw new ArgumentNullException(nameof(grouper)); - } - - var best = 0; - - var typedItems = ListItemsByType(types, recurse); - - var groups = new List>(); - var idx = 0; - - while (idx < typedItems.Count) - { - var a = typedItems[idx++]; - var group = new List - { - a - }; - - while (idx < typedItems.Count) - { - var b = typedItems[idx]; - var v = grouper(a, b); - - if (v == 0) - { - group.Add(b); - } - else - { - break; - } - - ++idx; - } - - groups.Add(group); - } - - for (var j = 0; j < groups.Count; ++j) - { - var items = groups[j].ToArray(); - var total = 0; - - foreach (var item in items) - { - total += item.Amount; - } - - if (total >= best) - { - best = total; - } - } - - return best; - } - - public int GetBestGroupAmount(Type[][] types, bool recurse, CheckItemGroup grouper) - { - if (grouper == null) - { - throw new ArgumentNullException(nameof(grouper)); - } - - var best = 0; - - for (var i = 0; i < types.Length; ++i) - { - using var typedItems = ListItemsByType(types[i], recurse); - - var groups = new List>(); - var idx = 0; - - while (idx < typedItems.Count) - { - var a = typedItems[idx++]; - var group = new List - { - a - }; - - while (idx < typedItems.Count) - { - var b = typedItems[idx]; - var v = grouper(a, b); - - if (v == 0) - { - group.Add(b); - } - else - { - break; - } - - ++idx; - } - - groups.Add(group); - } - - for (var j = 0; j < groups.Count; ++j) - { - var items = groups[j].ToArray(); - var total = 0; - - for (var k = 0; k < items.Length; ++k) - { - total += items[k].Amount; - } - - if (total >= best) - { - best = total; - } - } - } - - return best; + ArgumentNullException.ThrowIfNull(grouper); + using var items = ListItemsByType(types, recurse); + return BestGroupTotal(items, grouper); } public int GetAmount(Type type, bool recurse = true) @@ -1489,6 +883,7 @@ public partial class Container : Item return total; } + public Item FindItemByType(Type type, bool recurse = true) { foreach (var item in FindItems(recurse)) @@ -1543,27 +938,112 @@ public partial class Container : Item return null; } - private struct ItemStackEntry + // Sums Item.Amount of all items matching `type`, bailing as soon as + // `amount` is reached. Walks the alloc-free FindItemsByType enumerator — + // no list materialization. Used as the Phase-1 sufficiency check by the + // ConsumeTotal overloads. + private bool HasAmount(Type type, int amount, bool recurse) { - public readonly Item m_StackItem; - public readonly Item m_DropItem; - - public ItemStackEntry(Item stack, Item drop) + var total = 0; + foreach (var item in FindItemsByType(type, recurse)) { - m_StackItem = stack; - m_DropItem = drop; + total += item.Amount; + if (total >= amount) + { + return true; + } + } + return false; + } + + // Walks `items` (BFS-ordered snapshot) in adjacency-based groups defined + // by `grouper`. Returns the first group whose Amount sum is >= `amount`, + // emitting its slice [start, start+length). Streaming, no per-group list. + private static bool TryFindGroupMeetingAmount( + PooledRefList items, int amount, CheckItemGroup grouper, out int groupStart, out int groupLength + ) + { + var i = 0; + while (i < items.Count) + { + var leader = items[i]; + var start = i; + var total = leader.Amount; + i++; + while (i < items.Count && grouper(leader, items[i]) == 0) + { + total += items[i].Amount; + i++; + } + if (total >= amount) + { + groupStart = start; + groupLength = i - start; + return true; + } + } + groupStart = 0; + groupLength = 0; + return false; + } + + // Returns the largest group sum across `items` partitioned by `grouper`. + // Streaming, no per-group list. + private static int BestGroupTotal(PooledRefList items, CheckItemGroup grouper) + { + var best = 0; + var i = 0; + while (i < items.Count) + { + var leader = items[i]; + var total = leader.Amount; + i++; + while (i < items.Count && grouper(leader, items[i]) == 0) + { + total += items[i].Amount; + i++; + } + if (total > best) + { + best = total; + } + } + return best; + } + + // Consumes `need` units from the slice [start, start+length) of `items`, + // firing `callback` once per item touched with the actual delta. Items + // with Amount <= delta are deleted via Item.Consume. + private static void ConsumeSlice(PooledRefList items, int start, int length, int need, OnItemConsumed callback) + { + for (var k = 0; k < length && need > 0; k++) + { + var item = items[start + k]; + var theirAmount = item.Amount; + if (theirAmount < need) + { + callback?.Invoke(item, theirAmount); + item.Consume(theirAmount); + need -= theirAmount; + } + else + { + callback?.Invoke(item, need); + item.Consume(need); + return; + } } } } public class ContainerData { - private static ILogger logger = LogFactory.GetLogger(typeof(ContainerData)); - private static readonly Dictionary m_Table; + private static readonly ILogger _logger = LogFactory.GetLogger(typeof(ContainerData)); + private static readonly Dictionary _table; static ContainerData() { - m_Table = new Dictionary(); + _table = new Dictionary(); var path = Path.Combine(Core.BaseDirectory, "Data/containers.cfg"); @@ -1621,13 +1101,13 @@ public class ContainerData { var id = Utility.ToInt32(aIDs[i]); - if (m_Table.ContainsKey(id)) + if (_table.ContainsKey(id)) { - logger.Warning("double ItemID entry in Data\\containers.cfg"); + _logger.Warning("double ItemID entry in Data\\containers.cfg"); } else { - m_Table[id] = data; + _table[id] = data; } } } @@ -1660,7 +1140,7 @@ public class ContainerData public static ContainerData GetData(int itemID) { - m_Table.TryGetValue(itemID, out var data); + _table.TryGetValue(itemID, out var data); return data ?? Default; } } diff --git a/Projects/Server/Items/Item.Enumerable.cs b/Projects/Server/Items/Item.Enumerable.cs index df81c2118..417c9df58 100644 --- a/Projects/Server/Items/Item.Enumerable.cs +++ b/Projects/Server/Items/Item.Enumerable.cs @@ -54,16 +54,16 @@ public partial class Item /// . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public FindItemsByTypeEnumerator FindItemsByType(bool recurse = true, Predicate predicate = null) where T : Item => - new(this, recurse, predicate); + public FindItemsByTypeEnumerator FindItemsByType(bool recurse = true, Predicate predicate = null) + where T : Item => new(this, recurse, predicate); [MethodImpl(MethodImplOptions.AggressiveInlining)] public FindItemsByTypeEnumerator FindItemsByType(Type type, bool recurse = true) => - new(this, recurse, type.IsInstanceOfType); + new(this, recurse, type); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public FindItemsByTypeEnumerator FindItemsByType(Type[] types, bool recurse = true) => - new(this, recurse, item => item.InTypeList(types)); + public FindItemsByTypeEnumerator FindItemsByType(ReadOnlySpan types, bool recurse = true) => + new(this, recurse, types); [MethodImpl(MethodImplOptions.AggressiveInlining)] public FindItemsByTypeEnumerator FindItems(bool recurse = true, Predicate predicate = null) => @@ -122,28 +122,22 @@ public partial class Item { var queue = PooledRefQueue.Create(128); - foreach (var item in FindItemsByType(recurse)) + foreach (var item in FindItemsByType(type, recurse)) { - if (type.IsInstanceOfType(item)) - { - queue.Enqueue(item); - } + queue.Enqueue(item); } return queue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledRefQueue EnumerateItemsByType(Type[] types, bool recurse = true) + public PooledRefQueue EnumerateItemsByType(ReadOnlySpan types, bool recurse = true) { var queue = PooledRefQueue.Create(128); - foreach (var item in FindItemsByType(recurse)) + foreach (var item in FindItemsByType(types, recurse)) { - if (item.InTypeList(types)) - { - queue.Enqueue(item); - } + queue.Enqueue(item); } return queue; @@ -170,28 +164,22 @@ public partial class Item { var list = PooledRefList.Create(128); - foreach (var item in FindItemsByType(recurse)) + foreach (var item in FindItemsByType(type, recurse)) { - if (type.IsInstanceOfType(item)) - { - list.Add(item); - } + list.Add(item); } return list; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledRefList ListItemsByType(Type[] types, bool recurse = true) + public PooledRefList ListItemsByType(ReadOnlySpan types, bool recurse = true) { var list = PooledRefList.Create(128); - foreach (var item in FindItemsByType(recurse)) + foreach (var item in FindItemsByType(types, recurse)) { - if (item.InTypeList(types)) - { - list.Add(item); - } + list.Add(item); } return list; @@ -211,13 +199,30 @@ public partial class Item private int _index; private T _current; private readonly bool _recurse; - private readonly Predicate _predicate; private Item _currentContainer; private int _version; + // Exactly one filter source is used per enumerator instance, depending on + // which constructor was called. The unused fields stay at default and the + // branches below pick the right path. This avoids the per-call delegate + // allocation that the (Type)/(Type[]) factory methods used to incur. + private readonly Predicate _predicate; + private readonly Type _runtimeType; + private readonly ReadOnlySpan _runtimeTypes; + public FindItemsByTypeEnumerator(Item container, bool recurse, Predicate predicate) + : this(container, recurse) => _predicate = predicate; + + public FindItemsByTypeEnumerator(Item container, bool recurse, Type runtimeType) + : this(container, recurse) => _runtimeType = runtimeType; + + public FindItemsByTypeEnumerator(Item container, bool recurse, ReadOnlySpan runtimeTypes) + : this(container, recurse) => _runtimeTypes = runtimeTypes; + + private FindItemsByTypeEnumerator(Item container, bool recurse) { - _containers = PooledRefQueue.Create(_recurse ? 64 : 0); + _recurse = recurse; + _containers = PooledRefQueue.Create(recurse ? 64 : 0); if (container != null) { @@ -230,11 +235,6 @@ public partial class Item _currentContainer = container; _version = container.LookupContainerVersion(); } - - _current = default; - _index = 0; - _recurse = recurse; - _predicate = predicate; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -259,6 +259,22 @@ public partial class Item return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool Matches(T t) + { + if (_runtimeType is not null) + { + return _runtimeType.IsInstanceOfType(t); + } + + if (_runtimeTypes.Length > 0) + { + return t.GetType().InTypeList(_runtimeTypes); + } + + return _predicate?.Invoke(t) != false; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool SetNextItem() { @@ -270,12 +286,12 @@ public partial class Item while (_index < _items.Length) { var item = _items[_index++]; - if (_recurse && item.LookupItems() is { Count: > 0 } items) + if (_recurse && item.LookupItems() is { Count: > 0 }) { _containers.Enqueue(item); } - if (item is T t && _predicate?.Invoke(t) != false) + if (item is T t && Matches(t)) { if (_version != _currentContainer.LookupContainerVersion()) { diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index c75c3fb38..fbb79a414 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -1705,13 +1705,13 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert } } - public List LookupItems() => (this is Container container ? container.m_Items : LookupCompactInfo()?.m_Items) ?? EmptyItems; + public List LookupItems() => (this is Container container ? container._items : LookupCompactInfo()?.m_Items) ?? EmptyItems; public List AcquireItems() { if (this is Container cont) { - return cont.m_Items ??= new List(); + return cont._items ??= new List(); } var info = AcquireCompactInfo(); @@ -2714,7 +2714,7 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert if (this is Container) { - (this as Container).m_Items = items; + (this as Container)._items = items; } else { @@ -2875,7 +2875,7 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert if (this is Container cont) { - cont.m_Items = items; + cont._items = items; } else { @@ -3004,7 +3004,7 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert if (this is Container cont) { - cont.m_Items = items; + cont._items = items; } else { diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 333e1e7bd..4f33220aa 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -1422,9 +1422,9 @@ public static partial class Utility public static bool IsNullOrWhiteSpace(this ReadOnlySpan span) => span.IsEmpty || span.IsWhiteSpace(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool InTypeList(this T obj, Type[] types) => obj.GetType().InTypeList(types); + public static bool InTypeList(this T obj, ReadOnlySpan types) => obj.GetType().InTypeList(types); - public static bool InTypeList(this Type t, Type[] types) + public static bool InTypeList(this Type t, ReadOnlySpan types) { for (var i = 0; i < types.Length; ++i) { diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 3fc9c0d8d..e8bdbc394 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -3289,9 +3289,9 @@ namespace Server.Mobiles var (totalFame, totalKarma) = Titles.ComputeKillAwards(this, Map); var list = GetLootingRights(DamageEntries, HitsMax); - var titles = new List(); - var fame = new List(); - var karma = new List(); + using var titles = PooledRefList.Create(); + var fame = PooledRefList.Create(); + var karma = PooledRefList.Create(); var givenQuestKill = false; var givenFactionKill = false; @@ -3396,6 +3396,9 @@ namespace Server.Mobiles Titles.AwardFame(titles[i], fame[i], true); Titles.AwardKarma(titles[i], karma[i], true); } + + fame.Dispose(); + karma.Dispose(); } base.OnDeath(c);