diff --git a/Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs index 68a867728..b2eb644ea 100644 --- a/Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs +++ b/Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs @@ -476,6 +476,63 @@ public class ClientEnumeratorTests return (ns, mobile); } + [Fact] + public void ClientEnumerator_ZeroRangeReturnsOnlyCenter() + { + var map = Map.Felucca; + var center = new Point3D(950, 950, 0); + const int range = 0; + + var clients = new (NetState, Mobile)[2]; + try + { + clients[0] = CreateClientWithMobile(map, center); // Exact center + clients[1] = CreateClientWithMobile(map, new Point3D(951, 950, 0)); // 1 tile away + + var found = new List(); + foreach (var ns in map.GetClientsInRange(center, range)) + { + found.Add(ns); + } + + Assert.Single(found); + Assert.Equal(clients[0].Item1, found[0]); + } + finally + { + DeleteAll(clients); + } + } + + [Fact] + public void ClientEnumerator_NegativeRangeCreates1x1Bounds() + { + var map = Map.Felucca; + var center = new Point3D(1050, 1050, 0); + const int range = -5; + + var clients = new (NetState, Mobile)[2]; + try + { + clients[0] = CreateClientWithMobile(map, center); + clients[1] = CreateClientWithMobile(map, new Point3D(1051, 1050, 0)); // 1 tile away + + var found = new List(); + foreach (var ns in map.GetClientsInRange(center, range)) + { + found.Add(ns); + } + + // With negative range creating a 1x1 bounds, only exact center matches + Assert.Single(found); + Assert.Equal(clients[0].Item1, found[0]); + } + finally + { + DeleteAll(clients); + } + } + private static void DeleteAll((NetState, Mobile)[] clients) { for (var i = 0; i < clients.Length; i++) diff --git a/Projects/Server.Tests/Tests/Maps/ItemByDistanceEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/ItemByDistanceEnumeratorTests.cs new file mode 100644 index 000000000..be259044f --- /dev/null +++ b/Projects/Server.Tests/Tests/Maps/ItemByDistanceEnumeratorTests.cs @@ -0,0 +1,605 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace Server.Tests.Tests.Maps; + +[Collection("Sequential Server Tests")] +public class ItemByDistanceEnumeratorTests +{ + [Fact] + public void ItemByDistanceEnumerator_ReturnsNearbyItems() + { + var map = Map.Felucca; + var center = new Point3D(100, 100, 0); + const int range = 5; + + var Items = new TestItem[3]; + try + { + Items[0] = CreateItem(map, new Point3D(102, 102, 0)); // Within range + Items[1] = CreateItem(map, new Point3D(98, 98, 0)); // Within range + Items[2] = CreateItem(map, new Point3D(110, 110, 0)); // Outside range + + var found = new List(); + foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range)) + { + found.Add(Item); + } + + Assert.Equal(2, found.Count); + Assert.Contains(Items[0], found); + Assert.Contains(Items[1], found); + Assert.DoesNotContain(Items[2], found); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_DeletedItemsAreSkipped() + { + var map = Map.Felucca; + var center = new Point3D(200, 200, 0); + const int range = 5; + + var Items = new TestItem[3]; + try + { + Items[0] = CreateItem(map, new Point3D(202, 202, 0)); + Items[1] = CreateItem(map, new Point3D(203, 202, 0)); + Items[2] = CreateItem(map, new Point3D(204, 202, 0)); + + Items[1].Delete(); + + var found = new List(); + foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range)) + { + found.Add(Item); + } + + Assert.Equal(2, found.Count); + Assert.Contains(Items[0], found); + Assert.Contains(Items[2], found); + Assert.DoesNotContain(Items[1], found); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_ReturnsMinDistance() + { + var map = Map.Felucca; + var center = new Point3D(300, 300, 0); + const int range = 10; + + var Items = new TestItem[2]; + try + { + Items[0] = CreateItem(map, new Point3D(305, 305, 0)); + Items[1] = CreateItem(map, new Point3D(302, 302, 0)); + + var foundWithDistance = new List<(Item, int)>(); + foreach (var result in map.GetItemsInRangeByDistance(center, range)) + { + foundWithDistance.Add(result); + } + + Assert.Equal(2, foundWithDistance.Count); + // Each Item should have a non-negative min distance + Assert.All(foundWithDistance, item => Assert.True(item.Item2 >= 0)); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_OrderedBySector() + { + var map = Map.Felucca; + var center = new Point3D(400, 400, 0); + const int range = Map.SectorSize * 2; + + var Items = new TestItem[3]; + try + { + // Place Items in different sectors + Items[0] = CreateItem(map, new Point3D(center.X + 2, center.Y + 2, 0)); + Items[1] = CreateItem(map, new Point3D(center.X + Map.SectorSize + 2, center.Y + 2, 0)); + Items[2] = CreateItem(map, new Point3D(center.X + 2, center.Y + Map.SectorSize + 2, 0)); + + var found = new List(); + foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range)) + { + found.Add(Item); + } + + Assert.Equal(3, found.Count); + Assert.Contains(Items[0], found); + Assert.Contains(Items[1], found); + Assert.Contains(Items[2], found); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_MapNullYieldsEmpty() + { + var center = new Point2D(0, 0); + var bounds = new Rectangle2D(center.m_X - 10, center.m_Y - 10, 21, 21); + var enumerator = new Map.ItemDistanceEnumerable(null, bounds, center, false).GetEnumerator(); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void ItemByDistanceEnumerator_ThrowsOnVersionChange() + { + var map = Map.Felucca; + var center = new Point3D(500, 500, 0); + const int range = 5; + + var Items = new[] + { + CreateItem(map, new Point3D(502, 502, 0)), + CreateItem(map, new Point3D(503, 502, 0)) + }; + + try + { + var enumerator = map.GetItemsInRangeByDistance(center, range).GetEnumerator(); + Assert.True(enumerator.MoveNext()); + + Items[1].Delete(); + + // Ref structs cannot be captured in lambdas, so we test the exception directly + var exceptionThrown = false; + try + { + enumerator.MoveNext(); + } + catch (InvalidOperationException) + { + exceptionThrown = true; + } + + Assert.True(exceptionThrown, "Expected InvalidOperationException when collection version changes"); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_ZeroRangeReturnsOnlyCenter() + { + var map = Map.Felucca; + var center = new Point3D(600, 600, 0); + const int range = 0; + + var Items = new TestItem[2]; + try + { + Items[0] = CreateItem(map, center); // Exact center + Items[1] = CreateItem(map, new Point3D(601, 600, 0)); // 1 tile away + + var found = new List(); + foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range)) + { + found.Add(Item); + } + + Assert.Single(found); + Assert.Equal(Items[0], found[0]); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_FiltersByType() + { + var map = Map.Felucca; + var center = new Point3D(700, 700, 0); + const int range = 5; + + var testItem2 = new TestItem2((Serial)Utility.RandomMinMax(0x100u, 0xFFFu)); + var testItem = new TestItem((Serial)Utility.RandomMinMax(0x100u, 0xFFFu)); + + try + { + testItem2.MoveToWorld(new Point3D(702, 702, 0), map); + testItem.MoveToWorld(new Point3D(703, 702, 0), map); + + var foundPlayers = new List(); + foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range)) + { + foundPlayers.Add(Item); + } + + Assert.Single(foundPlayers); + Assert.Equal(testItem2, foundPlayers[0]); + } + finally + { + testItem2.Delete(); + testItem.Delete(); + } + } + + [Fact] + public void ItemByDistanceEnumerator_UsesDifferentPointOverloads() + { + var map = Map.Felucca; + var center = new Point3D(800, 800, 5); + const int range = 5; + + var Items = new TestItem[1]; + try + { + Items[0] = CreateItem(map, new Point3D(802, 802, 0)); + + // Test Point3D overload + var found1 = new List(); + foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range)) + { + found1.Add(Item); + } + + // Test (int, int) overload + var found2 = new List(); + foreach (var (Item, _) in map.GetItemsInRangeByDistance(center.X, center.Y, range)) + { + found2.Add(Item); + } + + // Test Point2D overload + var found3 = new List(); + foreach (var (Item, _) in map.GetItemsInRangeByDistance(new Point2D(center.X, center.Y), range)) + { + found3.Add(Item); + } + + Assert.Single(found1); + Assert.Equal(Items[0], found1[0]); + Assert.Equal(found1, found2); + Assert.Equal(found1, found3); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_RingTraversal() + { + var map = Map.Felucca; + var center = new Point3D(900, 900, 0); + const int range = Map.SectorSize * 2; + + var Items = new TestItem[4]; + try + { + // Place Items in different rings around the center + Items[0] = CreateItem(map, center); // Ring 0 (center sector) + Items[1] = CreateItem(map, new Point3D(center.X + Map.SectorSize, center.Y, 0)); // Ring 1 + Items[2] = CreateItem(map, new Point3D(center.X, center.Y + Map.SectorSize, 0)); // Ring 1 + Items[3] = CreateItem(map, new Point3D(center.X + Map.SectorSize * 2 - 1, center.Y, 0)); // Ring 2 + + var found = new List(); + var distances = new List(); + foreach (var (Item, minDistance) in map.GetItemsInRangeByDistance(center, range)) + { + found.Add(Item); + distances.Add(minDistance); + } + + // All Items should be found + Assert.Equal(4, found.Count); + Assert.Contains(Items[0], found); + Assert.Contains(Items[1], found); + Assert.Contains(Items[2], found); + Assert.Contains(Items[3], found); + + // Items should be processed by sector distance (ring-based) + // The center Item should have distance 0 + var centerIndex = found.IndexOf(Items[0]); + Assert.Equal(0, distances[centerIndex]); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_MapBoundsAreClamped() + { + var map = Map.Felucca; + var width = map.Width; + var height = map.Height; + + var center = new Point3D(width - 2, height - 2, 0); + const int range = Map.SectorSize * 2; + + var Items = new TestItem[1]; + try + { + Items[0] = CreateItem(map, center); + + var found = new List(); + foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range)) + { + found.Add(Item); + } + + Assert.Single(found); + Assert.Equal(Items[0], found[0]); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_NegativeRangeIsZero() + { + var map = Map.Felucca; + var center = new Point3D(1000, 1000, 0); + const int range = -5; + + var Items = new TestItem[2]; + try + { + Items[0] = CreateItem(map, center); + Items[1] = CreateItem(map, new Point3D(1001, 1000, 0)); // 1 tile away + + var found = new List(); + foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range)) + { + found.Add(Item); + } + + // With negative range creating a 1x1 bounds, only exact center matches + Assert.Single(found); + Assert.Equal(Items[0], found[0]); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_MultipleRings() + { + var map = Map.Felucca; + var center = new Point3D(1100, 1100, 0); + const int range = Map.SectorSize * 3; + + var Items = new List(); + try + { + // Create a grid of Items across multiple sectors + for (var ringOffset = 0; ringOffset <= 2; ringOffset++) + { + for (var side = 0; side < 4; side++) + { + var offset = ringOffset * Map.SectorSize; + Point3D pos = side switch + { + 0 => new Point3D(center.X + offset, center.Y, 0), + 1 => new Point3D(center.X, center.Y + offset, 0), + 2 => new Point3D(center.X - offset, center.Y, 0), + _ => new Point3D(center.X, center.Y - offset, 0) + }; + Items.Add(CreateItem(map, pos)); + } + } + + var found = new List(); + foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range)) + { + found.Add(Item); + } + + // Should find all Items within range + Assert.True(found.Count > 0); + Assert.All(found, Item => + { + var dx = Item.X - center.X; + var dy = Item.Y - center.Y; + var distSq = dx * dx + dy * dy; + Assert.True(distSq <= range * range); + }); + } + finally + { + foreach (var Item in Items) + { + Item?.Delete(); + } + } + } + + private static TestItem CreateItem(Map map, Point3D location) + { + var Item = new TestItem((Serial)Utility.RandomMinMax(0x100u, 0xFFFu)); + Item.MoveToWorld(location, map); + return Item; + } + + private static void DeleteAll(TestItem[] Items) + { + for (var i = 0; i < Items.Length; i++) + { + Items[i]?.Delete(); + } + } + + [Fact] + public void ItemByDistanceEnumerator_Bounds_FindsItemsInBounds() + { + var map = Map.Felucca; + var bounds = new Rectangle2D(100, 100, 50, 50); + + var Items = new TestItem[3]; + try + { + // Item inside bounds + Items[0] = CreateItem(map, new Point3D(120, 120, 0)); + // Item at edge of bounds + Items[1] = CreateItem(map, new Point3D(149, 149, 0)); + // Item outside bounds + Items[2] = CreateItem(map, new Point3D(200, 200, 0)); + + var found = new List(); + foreach (var (Item, _) in map.GetItemsInBoundsByDistance(bounds)) + { + found.Add(Item); + } + + Assert.Equal(2, found.Count); + Assert.Contains(Items[0], found); + Assert.Contains(Items[1], found); + Assert.DoesNotContain(Items[2], found); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_Bounds_MakeBoundsInclusive() + { + var map = Map.Felucca; + var bounds = new Rectangle2D(100, 100, 50, 50); + + var Items = new TestItem[2]; + try + { + // Item at edge (inclusive) + Items[0] = CreateItem(map, new Point3D(149, 149, 0)); + // Item just outside edge (will be included with makeBoundsInclusive) + Items[1] = CreateItem(map, new Point3D(150, 150, 0)); + + var foundWithoutInclusive = new List(); + foreach (var (Item, _) in map.GetItemsInBoundsByDistance(bounds)) + { + foundWithoutInclusive.Add(Item); + } + + var foundWithInclusive = new List(); + foreach (var (Item, _) in map.GetItemsInBoundsByDistance(bounds, true)) + { + foundWithInclusive.Add(Item); + } + + Assert.Single(foundWithoutInclusive); + Assert.Contains(Items[0], foundWithoutInclusive); + + Assert.Equal(2, foundWithInclusive.Count); + Assert.Contains(Items[0], foundWithInclusive); + Assert.Contains(Items[1], foundWithInclusive); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_Bounds_ReturnsMinDistance() + { + var map = Map.Felucca; + var bounds = new Rectangle2D(300, 300, 20, 20); + + var Items = new TestItem[2]; + try + { + Items[0] = CreateItem(map, new Point3D(305, 305, 0)); + Items[1] = CreateItem(map, new Point3D(315, 315, 0)); + + var foundWithDistance = new List<(Item, int)>(); + foreach (var result in map.GetItemsInBoundsByDistance(bounds)) + { + foundWithDistance.Add(result); + } + + Assert.Equal(2, foundWithDistance.Count); + Assert.All(foundWithDistance, item => Assert.True(item.Item2 >= 0)); + } + finally + { + DeleteAll(Items); + } + } + + [Fact] + public void ItemByDistanceEnumerator_Bounds_OrdersByProximityToCenter() + { + var map = Map.Felucca; + var bounds = new Rectangle2D(500, 500, 64, 64); + + var Items = new TestItem[3]; + try + { + // Place Items at different distances from center + Items[0] = CreateItem(map, new Point3D(532, 532, 0)); // At center + Items[1] = CreateItem(map, new Point3D(548, 532, 0)); // 16 tiles away + Items[2] = CreateItem(map, new Point3D(563, 563, 0)); // Far corner + + var found = new List<(Item, int)>(); + foreach (var result in map.GetItemsInBoundsByDistance(bounds)) + { + found.Add(result); + } + + Assert.Equal(3, found.Count); + + // Verify ordering by distance - closer Items should be found earlier (lower minDistance) + var Item0Index = found.FindIndex(x => x.Item1 == Items[0]); + var Item1Index = found.FindIndex(x => x.Item1 == Items[1]); + var Item2Index = found.FindIndex(x => x.Item1 == Items[2]); + + // The minDistance should increase (or stay the same) as we go through the list + Assert.True(found[Item0Index].Item2 <= found[Item1Index].Item2); + Assert.True(found[Item1Index].Item2 <= found[Item2Index].Item2); + } + finally + { + DeleteAll(Items); + } + } + + // Test implementation of Item + private class TestItem : Item + { + public TestItem(Serial serial) : base(serial) + { + } + } + + private class TestItem2 : Item + { + public TestItem2(Serial serial) : base(serial) + { + } + } +} + diff --git a/Projects/Server.Tests/Tests/Maps/ItemEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/ItemEnumeratorTests.cs index ef37ade80..1a9495008 100644 --- a/Projects/Server.Tests/Tests/Maps/ItemEnumeratorTests.cs +++ b/Projects/Server.Tests/Tests/Maps/ItemEnumeratorTests.cs @@ -408,6 +408,63 @@ public class ItemEnumeratorTests } } + [Fact] + public void ItemEnumerator_ZeroRangeReturnsOnlyCenter() + { + var map = Map.Felucca; + var center = new Point3D(850, 850, 0); + const int range = 0; + + var items = new Item[2]; + try + { + items[0] = CreateItem(map, center); // Exact center + items[1] = CreateItem(map, new Point3D(851, 850, 0)); // 1 tile away + + var found = new List(); + foreach (var item in map.GetItemsInRange(center, range)) + { + found.Add(item); + } + + Assert.Single(found); + Assert.Equal(items[0], found[0]); + } + finally + { + DeleteAll(items); + } + } + + [Fact] + public void ItemEnumerator_NegativeRangeCreates1x1Bounds() + { + var map = Map.Felucca; + var center = new Point3D(900, 900, 0); + const int range = -5; + + var items = new Item[2]; + try + { + items[0] = CreateItem(map, center); + items[1] = CreateItem(map, new Point3D(901, 900, 0)); // 1 tile away + + var found = new List(); + foreach (var item in map.GetItemsInRange(center, range)) + { + found.Add(item); + } + + // With negative range creating a 1x1 bounds, only exact center matches + Assert.Single(found); + Assert.Equal(items[0], found[0]); + } + finally + { + DeleteAll(items); + } + } + private static Item CreateItem(Map map, Point3D location) { var item = new Item(0x1); diff --git a/Projects/Server.Tests/Tests/Maps/MobileByDistanceEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/MobileByDistanceEnumeratorTests.cs new file mode 100644 index 000000000..435a53cd8 --- /dev/null +++ b/Projects/Server.Tests/Tests/Maps/MobileByDistanceEnumeratorTests.cs @@ -0,0 +1,608 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace Server.Tests.Tests.Maps; + +[Collection("Sequential Server Tests")] +public class MobileByDistanceEnumeratorTests +{ + [Fact] + public void MobileByDistanceEnumerator_ReturnsNearbyMobiles() + { + var map = Map.Felucca; + var center = new Point3D(100, 100, 0); + const int range = 5; + + var mobiles = new TestMobile[3]; + try + { + mobiles[0] = CreateMobile(map, new Point3D(102, 102, 0)); // Within range + mobiles[1] = CreateMobile(map, new Point3D(98, 98, 0)); // Within range + mobiles[2] = CreateMobile(map, new Point3D(110, 110, 0)); // Outside range + + var found = new List(); + foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range)) + { + found.Add(mobile); + } + + Assert.Equal(2, found.Count); + Assert.Contains(mobiles[0], found); + Assert.Contains(mobiles[1], found); + Assert.DoesNotContain(mobiles[2], found); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_DeletedMobilesAreSkipped() + { + var map = Map.Felucca; + var center = new Point3D(200, 200, 0); + const int range = 5; + + var mobiles = new TestMobile[3]; + try + { + mobiles[0] = CreateMobile(map, new Point3D(202, 202, 0)); + mobiles[1] = CreateMobile(map, new Point3D(203, 202, 0)); + mobiles[2] = CreateMobile(map, new Point3D(204, 202, 0)); + + mobiles[1].Delete(); + + var found = new List(); + foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range)) + { + found.Add(mobile); + } + + Assert.Equal(2, found.Count); + Assert.Contains(mobiles[0], found); + Assert.Contains(mobiles[2], found); + Assert.DoesNotContain(mobiles[1], found); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_ReturnsMinDistance() + { + var map = Map.Felucca; + var center = new Point3D(300, 300, 0); + const int range = 10; + + var mobiles = new TestMobile[2]; + try + { + mobiles[0] = CreateMobile(map, new Point3D(305, 305, 0)); + mobiles[1] = CreateMobile(map, new Point3D(302, 302, 0)); + + var foundWithDistance = new List<(Mobile, int)>(); + foreach (var result in map.GetMobilesInRangeByDistance(center, range)) + { + foundWithDistance.Add(result); + } + + Assert.Equal(2, foundWithDistance.Count); + // Each mobile should have a non-negative min distance + Assert.All(foundWithDistance, item => Assert.True(item.Item2 >= 0)); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_OrderedBySector() + { + var map = Map.Felucca; + var center = new Point3D(400, 400, 0); + const int range = Map.SectorSize * 2; + + var mobiles = new TestMobile[3]; + try + { + // Place mobiles in different sectors + mobiles[0] = CreateMobile(map, new Point3D(center.X + 2, center.Y + 2, 0)); + mobiles[1] = CreateMobile(map, new Point3D(center.X + Map.SectorSize + 2, center.Y + 2, 0)); + mobiles[2] = CreateMobile(map, new Point3D(center.X + 2, center.Y + Map.SectorSize + 2, 0)); + + var found = new List(); + foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range)) + { + found.Add(mobile); + } + + Assert.Equal(3, found.Count); + Assert.Contains(mobiles[0], found); + Assert.Contains(mobiles[1], found); + Assert.Contains(mobiles[2], found); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_MapNullYieldsEmpty() + { + var center = new Point2D(0, 0); + var bounds = new Rectangle2D(center.m_X - 10, center.m_Y - 10, 21, 21); + var enumerator = new Map.MobileDistanceEnumerable(null, bounds, center, false).GetEnumerator(); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void MobileByDistanceEnumerator_ThrowsOnVersionChange() + { + var map = Map.Felucca; + var center = new Point3D(500, 500, 0); + const int range = 5; + + var mobiles = new[] + { + CreateMobile(map, new Point3D(502, 502, 0)), + CreateMobile(map, new Point3D(503, 502, 0)) + }; + + try + { + var enumerator = map.GetMobilesInRangeByDistance(center, range).GetEnumerator(); + Assert.True(enumerator.MoveNext()); + + mobiles[1].Delete(); + + // Ref structs cannot be captured in lambdas, so we test the exception directly + var exceptionThrown = false; + try + { + enumerator.MoveNext(); + } + catch (InvalidOperationException) + { + exceptionThrown = true; + } + + Assert.True(exceptionThrown, "Expected InvalidOperationException when collection version changes"); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_ZeroRangeReturnsOnlyCenter() + { + var map = Map.Felucca; + var center = new Point3D(600, 600, 0); + const int range = 0; + + var mobiles = new TestMobile[2]; + try + { + mobiles[0] = CreateMobile(map, center); // Exact center + mobiles[1] = CreateMobile(map, new Point3D(601, 600, 0)); // 1 tile away + + var found = new List(); + foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range)) + { + found.Add(mobile); + } + + Assert.Single(found); + Assert.Equal(mobiles[0], found[0]); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_FiltersByType() + { + var map = Map.Felucca; + var center = new Point3D(700, 700, 0); + const int range = 5; + + var player = new TestPlayerMobile((Serial)Utility.RandomMinMax(0x100u, 0xFFFu)); + var npc = new TestMobile((Serial)Utility.RandomMinMax(0x100u, 0xFFFu)); + + try + { + player.DefaultMobileInit(); + npc.DefaultMobileInit(); + player.MoveToWorld(new Point3D(702, 702, 0), map); + npc.MoveToWorld(new Point3D(703, 702, 0), map); + + var foundPlayers = new List(); + foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range)) + { + foundPlayers.Add(mobile); + } + + Assert.Single(foundPlayers); + Assert.Equal(player, foundPlayers[0]); + } + finally + { + player.Delete(); + npc.Delete(); + } + } + + [Fact] + public void MobileByDistanceEnumerator_UsesDifferentPointOverloads() + { + var map = Map.Felucca; + var center = new Point3D(800, 800, 5); + const int range = 5; + + var mobiles = new TestMobile[1]; + try + { + mobiles[0] = CreateMobile(map, new Point3D(802, 802, 0)); + + // Test Point3D overload + var found1 = new List(); + foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range)) + { + found1.Add(mobile); + } + + // Test (int, int) overload + var found2 = new List(); + foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center.X, center.Y, range)) + { + found2.Add(mobile); + } + + // Test Point2D overload + var found3 = new List(); + foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(new Point2D(center.X, center.Y), range)) + { + found3.Add(mobile); + } + + Assert.Single(found1); + Assert.Equal(mobiles[0], found1[0]); + Assert.Equal(found1, found2); + Assert.Equal(found1, found3); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_RingTraversal() + { + var map = Map.Felucca; + var center = new Point3D(900, 900, 0); + const int range = Map.SectorSize * 2; + + var mobiles = new TestMobile[4]; + try + { + // Place mobiles in different rings around the center + mobiles[0] = CreateMobile(map, center); // Ring 0 (center sector) + mobiles[1] = CreateMobile(map, new Point3D(center.X + Map.SectorSize, center.Y, 0)); // Ring 1 + mobiles[2] = CreateMobile(map, new Point3D(center.X, center.Y + Map.SectorSize, 0)); // Ring 1 + mobiles[3] = CreateMobile(map, new Point3D(center.X + Map.SectorSize * 2 - 1, center.Y, 0)); // Ring 2 + + var found = new List(); + var distances = new List(); + foreach (var (mobile, minDistance) in map.GetMobilesInRangeByDistance(center, range)) + { + found.Add(mobile); + distances.Add(minDistance); + } + + // All mobiles should be found + Assert.Equal(4, found.Count); + Assert.Contains(mobiles[0], found); + Assert.Contains(mobiles[1], found); + Assert.Contains(mobiles[2], found); + Assert.Contains(mobiles[3], found); + + // Mobiles should be processed by sector distance (ring-based) + // The center mobile should have distance 0 + var centerIndex = found.IndexOf(mobiles[0]); + Assert.Equal(0, distances[centerIndex]); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_MapBoundsAreClamped() + { + var map = Map.Felucca; + var width = map.Width; + var height = map.Height; + + var center = new Point3D(width - 2, height - 2, 0); + const int range = Map.SectorSize * 2; + + var mobiles = new TestMobile[1]; + try + { + mobiles[0] = CreateMobile(map, center); + + var found = new List(); + foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range)) + { + found.Add(mobile); + } + + Assert.Single(found); + Assert.Equal(mobiles[0], found[0]); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_NegativeRangeIsZero() + { + var map = Map.Felucca; + var center = new Point3D(1000, 1000, 0); + const int range = -5; + + var mobiles = new TestMobile[2]; + try + { + mobiles[0] = CreateMobile(map, center); + mobiles[1] = CreateMobile(map, new Point3D(1001, 1000, 0)); + + var found = new List(); + foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range)) + { + found.Add(mobile); + } + + // With negative range creating a 1x1 bounds, only exact center matches + Assert.Single(found); + Assert.Equal(mobiles[0], found[0]); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_MultipleRings() + { + var map = Map.Felucca; + var center = new Point3D(1100, 1100, 0); + const int range = Map.SectorSize * 3; + + var mobiles = new List(); + try + { + // Create a grid of mobiles across multiple sectors + for (var ringOffset = 0; ringOffset <= 2; ringOffset++) + { + for (var side = 0; side < 4; side++) + { + var offset = ringOffset * Map.SectorSize; + Point3D pos = side switch + { + 0 => new Point3D(center.X + offset, center.Y, 0), + 1 => new Point3D(center.X, center.Y + offset, 0), + 2 => new Point3D(center.X - offset, center.Y, 0), + _ => new Point3D(center.X, center.Y - offset, 0) + }; + mobiles.Add(CreateMobile(map, pos)); + } + } + + var found = new List(); + foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range)) + { + found.Add(mobile); + } + + // Should find all mobiles within range + Assert.True(found.Count > 0); + Assert.All(found, mobile => + { + var dx = mobile.X - center.X; + var dy = mobile.Y - center.Y; + var distSq = dx * dx + dy * dy; + Assert.True(distSq <= range * range); + }); + } + finally + { + foreach (var mobile in mobiles) + { + mobile?.Delete(); + } + } + } + + private static TestMobile CreateMobile(Map map, Point3D location) + { + var mobile = new TestMobile((Serial)Utility.RandomMinMax(0x100u, 0xFFFu)); + mobile.DefaultMobileInit(); + mobile.MoveToWorld(location, map); + return mobile; + } + + private static void DeleteAll(TestMobile[] mobiles) + { + for (var i = 0; i < mobiles.Length; i++) + { + mobiles[i]?.Delete(); + } + } + + [Fact] + public void MobileByDistanceEnumerator_Bounds_FindsMobilesInBounds() + { + var map = Map.Felucca; + var bounds = new Rectangle2D(100, 100, 50, 50); + + var mobiles = new TestMobile[3]; + try + { + // Mobile inside bounds + mobiles[0] = CreateMobile(map, new Point3D(120, 120, 0)); + // Mobile at edge of bounds + mobiles[1] = CreateMobile(map, new Point3D(149, 149, 0)); + // Mobile outside bounds + mobiles[2] = CreateMobile(map, new Point3D(200, 200, 0)); + + var found = new List(); + foreach (var (mobile, _) in map.GetMobilesInBoundsByDistance(bounds)) + { + found.Add(mobile); + } + + Assert.Equal(2, found.Count); + Assert.Contains(mobiles[0], found); + Assert.Contains(mobiles[1], found); + Assert.DoesNotContain(mobiles[2], found); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_Bounds_MakeBoundsInclusive() + { + var map = Map.Felucca; + var bounds = new Rectangle2D(100, 100, 50, 50); + + var mobiles = new TestMobile[2]; + try + { + // Mobile at edge (inclusive) + mobiles[0] = CreateMobile(map, new Point3D(149, 149, 0)); + // Mobile just outside edge (will be included with makeBoundsInclusive) + mobiles[1] = CreateMobile(map, new Point3D(150, 150, 0)); + + var foundWithoutInclusive = new List(); + foreach (var (mobile, _) in map.GetMobilesInBoundsByDistance(bounds)) + { + foundWithoutInclusive.Add(mobile); + } + + var foundWithInclusive = new List(); + foreach (var (mobile, _) in map.GetMobilesInBoundsByDistance(bounds, true)) + { + foundWithInclusive.Add(mobile); + } + + Assert.Single(foundWithoutInclusive); + Assert.Contains(mobiles[0], foundWithoutInclusive); + + Assert.Equal(2, foundWithInclusive.Count); + Assert.Contains(mobiles[0], foundWithInclusive); + Assert.Contains(mobiles[1], foundWithInclusive); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_Bounds_ReturnsMinDistance() + { + var map = Map.Felucca; + var bounds = new Rectangle2D(300, 300, 20, 20); + + var mobiles = new TestMobile[2]; + try + { + mobiles[0] = CreateMobile(map, new Point3D(305, 305, 0)); + mobiles[1] = CreateMobile(map, new Point3D(315, 315, 0)); + + var foundWithDistance = new List<(Mobile, int)>(); + foreach (var result in map.GetMobilesInBoundsByDistance(bounds)) + { + foundWithDistance.Add(result); + } + + Assert.Equal(2, foundWithDistance.Count); + Assert.All(foundWithDistance, item => Assert.True(item.Item2 >= 0)); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileByDistanceEnumerator_Bounds_OrdersByProximityToCenter() + { + var map = Map.Felucca; + var bounds = new Rectangle2D(500, 500, 64, 64); + + var mobiles = new TestMobile[3]; + try + { + // Place mobiles at different distances from center + mobiles[0] = CreateMobile(map, new Point3D(532, 532, 0)); // At center + mobiles[1] = CreateMobile(map, new Point3D(548, 532, 0)); // 16 tiles away + mobiles[2] = CreateMobile(map, new Point3D(563, 563, 0)); // Far corner + + var found = new List<(Mobile, int)>(); + foreach (var result in map.GetMobilesInBoundsByDistance(bounds)) + { + found.Add(result); + } + + Assert.Equal(3, found.Count); + + // Verify ordering by distance - closer mobiles should be found earlier (lower minDistance) + var mobile0Index = found.FindIndex(x => x.Item1 == mobiles[0]); + var mobile1Index = found.FindIndex(x => x.Item1 == mobiles[1]); + var mobile2Index = found.FindIndex(x => x.Item1 == mobiles[2]); + + // The minDistance should increase (or stay the same) as we go through the list + Assert.True(found[mobile0Index].Item2 <= found[mobile1Index].Item2); + Assert.True(found[mobile1Index].Item2 <= found[mobile2Index].Item2); + } + finally + { + DeleteAll(mobiles); + } + } + + // Test implementation of Mobile + private class TestMobile : Mobile + { + public TestMobile(Serial serial) : base(serial) + { + } + } + + private class TestPlayerMobile : Mobile + { + public TestPlayerMobile(Serial serial) : base(serial) + { + } + } +} + diff --git a/Projects/Server.Tests/Tests/Maps/MobileEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/MobileEnumeratorTests.cs index 5c7786a92..f1c3117eb 100644 --- a/Projects/Server.Tests/Tests/Maps/MobileEnumeratorTests.cs +++ b/Projects/Server.Tests/Tests/Maps/MobileEnumeratorTests.cs @@ -188,6 +188,63 @@ public class MobileEnumeratorTests } } + [Fact] + public void MobileEnumerator_ZeroRangeReturnsOnlyCenter() + { + var map = Map.Felucca; + var center = new Point3D(700, 700, 0); + const int range = 0; + + var mobiles = new Mobile[2]; + try + { + mobiles[0] = CreateMobile(map, center); // Exact center + mobiles[1] = CreateMobile(map, new Point3D(701, 700, 0)); // 1 tile away + + var found = new List(); + foreach (var mobile in map.GetMobilesInRange(center, range)) + { + found.Add(mobile); + } + + Assert.Single(found); + Assert.Equal(mobiles[0], found[0]); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileEnumerator_NegativeRangeCreates1x1Bounds() + { + var map = Map.Felucca; + var center = new Point3D(750, 750, 0); + const int range = -5; + + var mobiles = new Mobile[2]; + try + { + mobiles[0] = CreateMobile(map, center); + mobiles[1] = CreateMobile(map, new Point3D(751, 750, 0)); // 1 tile away + + var found = new List(); + foreach (var mobile in map.GetMobilesInRange(center, range)) + { + found.Add(mobile); + } + + // With negative range creating a 1x1 bounds, only exact center matches + Assert.Single(found); + Assert.Equal(mobiles[0], found[0]); + } + finally + { + DeleteAll(mobiles); + } + } + private static Mobile CreateMobile(Map map, Point3D location) { var mobile = new Mobile((Serial)Utility.RandomMinMax(0x100u, 0xFFFu)); diff --git a/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs index 0f592ca83..d99f7bbad 100644 --- a/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs +++ b/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs @@ -120,9 +120,8 @@ public class MultiEnumeratorTests { enumerator.MoveNext(); } - catch (Exception e) + catch (InvalidOperationException) { - Assert.IsType(e); exceptionThrown = true; } @@ -330,6 +329,63 @@ public class MultiEnumeratorTests } } + [Fact] + public void MultiEnumerator_ZeroRangeReturnsOnlyCenter() + { + var map = Map.Felucca; + var center = new Point3D(800, 800, 0); + const int range = 0; + + var multis = new TestMulti[2]; + try + { + multis[0] = CreateMulti(map, center); // Exact center + multis[1] = CreateMulti(map, new Point3D(801, 800, 0)); // 1 tile away + + var found = new List(); + foreach (var multi in map.GetMultisInRange(center, range)) + { + found.Add(multi); + } + + Assert.Single(found); + Assert.Equal(multis[0], found[0]); + } + finally + { + DeleteAll(multis); + } + } + + [Fact] + public void MultiEnumerator_NegativeRangeCreates1x1Bounds() + { + var map = Map.Felucca; + var center = new Point3D(850, 850, 0); + const int range = -5; + + var multis = new TestMulti[2]; + try + { + multis[0] = CreateMulti(map, center); + multis[1] = CreateMulti(map, new Point3D(851, 850, 0)); // 1 tile away + + var found = new List(); + foreach (var multi in map.GetMultisInRange(center, range)) + { + found.Add(multi); + } + + // With negative range creating a 1x1 bounds, only exact center matches + Assert.Single(found); + Assert.Equal(multis[0], found[0]); + } + finally + { + DeleteAll(multis); + } + } + private static TestMulti CreateMulti(Map map, Point3D location) { var multi = new TestMulti(); diff --git a/Projects/Server/Maps/Map.ClientByDistanceEnumerator.cs b/Projects/Server/Maps/Map.ClientByDistanceEnumerator.cs new file mode 100644 index 000000000..6d324cbb3 --- /dev/null +++ b/Projects/Server/Maps/Map.ClientByDistanceEnumerator.cs @@ -0,0 +1,303 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: Map.ClientByDistanceEnumerator.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 Server.Collections; +using Server.Network; + +namespace Server; + +public partial class Map +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ClientDistanceEnumerable GetClientsInRangeByDistance(Point3D p) => + GetClientsInRangeByDistance(p, Core.GlobalMaxUpdateRange); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ClientDistanceEnumerable GetClientsInRangeByDistance(Point3D p, int range) => + GetClientsInRangeByDistance(p.m_X, p.m_Y, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ClientDistanceEnumerable GetClientsInRangeByDistance(Point2D p) => + GetClientsInRangeByDistance(p, Core.GlobalMaxUpdateRange); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ClientDistanceEnumerable GetClientsInRangeByDistance(Point2D p, int range) => + GetClientsInRangeByDistance(p.m_X, p.m_Y, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ClientDistanceEnumerable GetClientsInRangeByDistance(int x, int y, int range) + { + var clampedRange = Math.Max(0, range); + var edge = clampedRange * 2 + 1; + return GetClientsInBoundsByDistance( + new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge), + new Point2D(x, y) + ); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ClientDistanceEnumerable GetClientsInBoundsByDistance(Rectangle2D bounds) => + GetClientsInBoundsByDistance(bounds, new Point2D(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ClientDistanceEnumerable GetClientsInBoundsByDistance(Rectangle2D bounds, bool makeBoundsInclusive) => + GetClientsInBoundsByDistance(bounds, new Point2D(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2), makeBoundsInclusive); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ClientDistanceEnumerable GetClientsInBoundsByDistance( + Rectangle2D bounds, Point2D center, bool makeBoundsInclusive = false + ) => new(this, bounds, center, makeBoundsInclusive); + + public ref struct ClientDistanceEnumerable + { + private readonly Map _map; + private readonly Rectangle2D _bounds; + private readonly Point2D _center; + private readonly bool _makeBoundsInclusive; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ClientDistanceEnumerable(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive) + { + _map = map; + _bounds = bounds; + _center = center; + _makeBoundsInclusive = makeBoundsInclusive; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ClientDistanceEnumerator GetEnumerator() => new(_map, _bounds, _center, _makeBoundsInclusive); + } + + public ref struct ClientDistanceEnumerator + { + private Map _map; + private Point2D _center; + private Rectangle2D _bounds; + + private int _sectorStartX; + private int _maxRing; + + private int _ring; // -1 = uninitialized, then 0.._maxRing + private int _ringIndex; // Current index within the ring + + private int _currentSectorX; + private int _currentSectorY; + + private ref readonly ValueLinkList _linkList; + private int _currentVersion; + private NetState _current; + + private int _minDistance; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ClientDistanceEnumerator(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive) + { + _map = map; + _center = center; + _bounds = makeBoundsInclusive + ? new Rectangle2D(bounds.X, bounds.Y, bounds.Width + 1, bounds.Height + 1) + : bounds; + + _current = null; + + if (map != null) + { + var centerSectorX = center.m_X / SectorSize; + var centerSectorY = center.m_Y / SectorSize; + + map.CalculateSectors(_bounds, out _sectorStartX, out var sectorStartY, out var sectorEndX, out var sectorEndY); + + // Calculate max ring based on bounds + var dx = Math.Max(centerSectorX - _sectorStartX, sectorEndX - centerSectorX); + var dy = Math.Max(centerSectorY - sectorStartY, sectorEndY - centerSectorY); + _maxRing = Math.Max(dx, dy); + } + + _ring = -1; + _ringIndex = -1; + _currentSectorX = 0; + _currentSectorY = 0; + + _currentVersion = 0; + _minDistance = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + var map = _map; + + if (map == null) + { + return false; + } + + if (!Unsafe.IsNullRef(in _linkList) && _linkList.Version != _currentVersion) + { + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); + } + + NetState current = _current; + + while (true) + { + current = current?.Next; + + while (current == null) + { + while (!TryNextSectorInRing(out _currentSectorX, out _currentSectorY)) + { + // Current ring exhausted, try next ring + if (_ring >= _maxRing) + { + return false; // No more rings to search + } + + _ring++; + _ringIndex = -1; + } + + _linkList = ref map.GetRealSector(_currentSectorX, _currentSectorY).Clients; + _currentVersion = _linkList.Version; + current = _linkList._first; + + if (current != null) + { + _minDistance = MinDistSqToSectorRect(_center.m_X, _center.m_Y, _currentSectorX, _currentSectorY); + } + } + + var m = current.Mobile; + if (m?.Deleted == false && _bounds.Contains(m.Location)) + { + _current = current; + return true; + } + } + } + + public (NetState Value, int MinDistance) Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (_current, _minDistance); + } + + private bool TryNextSectorInRing(out int sx, out int sy) + { + if (_ring == 0) + { + // Center sector + if (_ringIndex < 0) + { + _ringIndex = 0; + sx = _center.m_X / SectorSize; + sy = _center.m_Y / SectorSize; + return sx >= _sectorStartX; + } + + sx = sy = 0; + return false; + } + + var totalSectors = _ring * 8; + + // Keep trying sectors in this ring until we find a valid one or exhaust the ring + while (true) + { + var nextIndex = _ringIndex + 1; + + if (nextIndex >= totalSectors) + { + sx = sy = 0; + return false; + } + + _ringIndex = nextIndex; + CalculatePositionFromIndex(nextIndex, out sx, out sy); + + if (sx >= _sectorStartX) + { + return true; + } + } + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CalculatePositionFromIndex(int index, out int x, out int y) + { + var centerSectorX = _center.m_X / SectorSize; + var centerSectorY = _center.m_Y / SectorSize; + + var ringSize = _ring * 2; + var startX = centerSectorX - _ring; + var startY = centerSectorY - _ring; + + if (index <= ringSize) // Top edge + { + x = startX + index; + y = startY; + } + else if (index <= ringSize * 2) // Right edge + { + x = startX + ringSize; + y = startY + (index - ringSize); + } + else if (index <= ringSize * 3) // Bottom edge + { + x = startX + ringSize - (index - ringSize * 2); + y = startY + ringSize; + } + else // Left edge + { + x = startX; + y = startY + ringSize - (index - ringSize * 3); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int MinDistSqToSectorRect(int cx, int cy, int sectorX, int sectorY) + { + var x0 = sectorX * SectorSize; + var y0 = sectorY * SectorSize; + var x1 = x0 + (SectorSize - 1); + var y1 = y0 + (SectorSize - 1); + + var dx = 0; + if (cx < x0) + { + dx = x0 - cx; + } + else if (cx > x1) + { + dx = cx - x1; + } + + var dy = 0; + if (cy < y0) + { + dy = y0 - cy; + } + else if (cy > y1) + { + dy = cy - y1; + } + + return dx * dx + dy * dy; + } + } +} diff --git a/Projects/Server/Maps/Map.ClientEnumerator.cs b/Projects/Server/Maps/Map.ClientEnumerator.cs index d4dea8e26..921affec9 100644 --- a/Projects/Server/Maps/Map.ClientEnumerator.cs +++ b/Projects/Server/Maps/Map.ClientEnumerator.cs @@ -46,8 +46,12 @@ public partial class Map GetClientsInRange(p.m_X, p.m_Y, range); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ClientBoundsEnumerable GetClientsInRange(int x, int y, int range) => - GetClientsInBounds(new Rectangle2D(x - range, y - range, range * 2 + 1, range * 2 + 1)); + public ClientBoundsEnumerable GetClientsInRange(int x, int y, int range) + { + var clampedRange = Math.Max(0, range); + var edge = clampedRange * 2 + 1; + return GetClientsInBounds(new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge)); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public ClientBoundsEnumerable GetClientsInBounds(Rectangle2D bounds, bool makeBoundsInclusive = false) => @@ -232,7 +236,6 @@ public partial class Map throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); } - Mobile m; NetState current = _current; ref Rectangle2D bounds = ref _bounds; var currentSectorX = _currentSectorX; @@ -267,7 +270,7 @@ public partial class Map current = _linkList._first; } - m = current.Mobile; + var m = current.Mobile; if (m?.Deleted == false && bounds.Contains(m.Location)) { _current = current; diff --git a/Projects/Server/Maps/Map.ItemByDistanceEnumerator.cs b/Projects/Server/Maps/Map.ItemByDistanceEnumerator.cs new file mode 100644 index 000000000..698fe8b89 --- /dev/null +++ b/Projects/Server/Maps/Map.ItemByDistanceEnumerator.cs @@ -0,0 +1,325 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: Map.ItemByDistanceEnumerator.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 Server.Collections; + +namespace Server; + +public partial class Map +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInRangeByDistance(Point3D p) => + GetItemsInRangeByDistance(p, Core.GlobalMaxUpdateRange); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInRangeByDistance(Point3D p, int range) => + GetItemsInRangeByDistance(p, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInRangeByDistance(Point3D p) where T : Item => + GetItemsInRangeByDistance(p, Core.GlobalMaxUpdateRange); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInRangeByDistance(Point3D p, int range) where T : Item => + GetItemsInRangeByDistance(p.m_X, p.m_Y, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInRangeByDistance(Point2D p) => + GetItemsInRangeByDistance(p, Core.GlobalMaxUpdateRange); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInRangeByDistance(Point2D p, int range) => + GetItemsInRangeByDistance(p, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInRangeByDistance(Point2D p) where T : Item => + GetItemsInRangeByDistance(p, Core.GlobalMaxUpdateRange); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInRangeByDistance(Point2D p, int range) where T : Item => + GetItemsInRangeByDistance(p.m_X, p.m_Y, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInRangeByDistance(int x, int y, int range) => + GetItemsInRangeByDistance(x, y, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInRangeByDistance(int x, int y, int range) where T : Item + { + var clampedRange = Math.Max(0, range); + var edge = clampedRange * 2 + 1; + return GetItemsInBoundsByDistance( + new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge), + new Point2D(x, y) + ); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInBoundsByDistance(Rectangle2D bounds) => + GetItemsInBoundsByDistance(bounds); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable GetItemsInBoundsByDistance(Rectangle2D bounds, bool makeBoundsInclusive = false) + where T : Item => + GetItemsInBoundsByDistance( + bounds, + new Point2D(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2), + makeBoundsInclusive + ); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ItemDistanceEnumerable GetItemsInBoundsByDistance( + Rectangle2D bounds, Point2D center, bool makeBoundsInclusive = false + ) where T : Item => new(this, bounds, center, makeBoundsInclusive); + + public ref struct ItemDistanceEnumerable where T : Item + { + private readonly Map _map; + private readonly Rectangle2D _bounds; + private readonly Point2D _center; + private readonly bool _makeBoundsInclusive; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerable(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive) + { + _map = map; + _bounds = bounds; + _center = center; + _makeBoundsInclusive = makeBoundsInclusive; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerator GetEnumerator() => new(_map, _bounds, _center, _makeBoundsInclusive); + } + + public ref struct ItemDistanceEnumerator where T : Item + { + private Map _map; + private Point2D _center; + private Rectangle2D _bounds; + + private int _sectorStartX; + private int _maxRing; + + private int _ring; // -1 = uninitialized, then 0.._maxRing + private int _ringIndex; // Current index within the ring + + private int _currentSectorX; + private int _currentSectorY; + + private ref readonly ValueLinkList _linkList; + private int _currentVersion; + private T _current; + + private int _minDistance; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDistanceEnumerator(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive) + { + _map = map; + _center = center; + _bounds = makeBoundsInclusive + ? new Rectangle2D(bounds.X, bounds.Y, bounds.Width + 1, bounds.Height + 1) + : bounds; + + _current = null; + + if (map != null) + { + var centerSectorX = center.m_X / SectorSize; + var centerSectorY = center.m_Y / SectorSize; + + map.CalculateSectors(_bounds, out _sectorStartX, out var sectorStartY, out var sectorEndX, out var sectorEndY); + + // Calculate max ring based on bounds + var dx = Math.Max(centerSectorX - _sectorStartX, sectorEndX - centerSectorX); + var dy = Math.Max(centerSectorY - sectorStartY, sectorEndY - centerSectorY); + _maxRing = Math.Max(dx, dy); + } + + _ring = -1; + _ringIndex = -1; + _currentSectorX = 0; + _currentSectorY = 0; + + _currentVersion = 0; + _minDistance = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + var map = _map; + + if (map == null) + { + return false; + } + + if (!Unsafe.IsNullRef(in _linkList) && _linkList.Version != _currentVersion) + { + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); + } + + Item current = _current; + + while (true) + { + current = current?.Next; + + while (current == null) + { + while (!TryNextSectorInRing(out _currentSectorX, out _currentSectorY)) + { + // Current ring exhausted, try next ring + if (_ring >= _maxRing) + { + return false; // No more rings to search + } + + _ring++; + _ringIndex = -1; + } + + _linkList = ref map.GetRealSector(_currentSectorX, _currentSectorY).Items; + _currentVersion = _linkList.Version; + current = _linkList._first; + + if (current != null) + { + _minDistance = MinDistSqToSectorRect(_center.m_X, _center.m_Y, _currentSectorX, _currentSectorY); + } + } + + if (current is T { Deleted: false } o && _bounds.Contains(o.Location)) + { + _current = o; + return true; + } + } + } + + public (T Value, int MinDistance) Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (_current, _minDistance); + } + private bool TryNextSectorInRing(out int sx, out int sy) + { + if (_ring == 0) + { + // Center sector + if (_ringIndex < 0) + { + _ringIndex = 0; + sx = _center.m_X / SectorSize; + sy = _center.m_Y / SectorSize; + return sx >= _sectorStartX; + } + + sx = sy = 0; + return false; + } + + var totalSectors = _ring * 8; + + // Keep trying sectors in this ring until we find a valid one or exhaust the ring + while (true) + { + var nextIndex = _ringIndex + 1; + + if (nextIndex >= totalSectors) + { + sx = sy = 0; + return false; + } + + _ringIndex = nextIndex; + CalculatePositionFromIndex(nextIndex, out sx, out sy); + + if (sx >= _sectorStartX) + { + return true; + } + } + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CalculatePositionFromIndex(int index, out int x, out int y) + { + var centerSectorX = _center.m_X / SectorSize; + var centerSectorY = _center.m_Y / SectorSize; + + var ringSize = _ring * 2; + var startX = centerSectorX - _ring; + var startY = centerSectorY - _ring; + + if (index <= ringSize) // Top edge + { + x = startX + index; + y = startY; + } + else if (index <= ringSize * 2) // Right edge + { + x = startX + ringSize; + y = startY + (index - ringSize); + } + else if (index <= ringSize * 3) // Bottom edge + { + x = startX + ringSize - (index - ringSize * 2); + y = startY + ringSize; + } + else // Left edge + { + x = startX; + y = startY + ringSize - (index - ringSize * 3); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int MinDistSqToSectorRect(int cx, int cy, int sectorX, int sectorY) + { + var x0 = sectorX * SectorSize; + var y0 = sectorY * SectorSize; + var x1 = x0 + (SectorSize - 1); + var y1 = y0 + (SectorSize - 1); + + var dx = 0; + if (cx < x0) + { + dx = x0 - cx; + } + else if (cx > x1) + { + dx = cx - x1; + } + + var dy = 0; + if (cy < y0) + { + dy = y0 - cy; + } + else if (cy > y1) + { + dy = cy - y1; + } + + return dx * dx + dy * dy; + } + } +} diff --git a/Projects/Server/Maps/Map.ItemEnumerator.cs b/Projects/Server/Maps/Map.ItemEnumerator.cs index 4bf8f4202..f70c45172 100644 --- a/Projects/Server/Maps/Map.ItemEnumerator.cs +++ b/Projects/Server/Maps/Map.ItemEnumerator.cs @@ -69,8 +69,12 @@ public partial class Map GetItemsInRange(p.m_X, p.m_Y, range); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ItemBoundsEnumerable GetItemsInRange(int x, int y, int range) where T : Item => - GetItemsInBounds(new Rectangle2D(x - range, y - range, range * 2 + 1, range * 2 + 1)); + public ItemBoundsEnumerable GetItemsInRange(int x, int y, int range) where T : Item + { + var clampedRange = Math.Max(0, range); + var edge = clampedRange * 2 + 1; + return GetItemsInBounds(new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge)); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemBoundsEnumerable GetItemsInBounds(Rectangle2D bounds) => GetItemsInBounds(bounds); diff --git a/Projects/Server/Maps/Map.MobileByDistanceEnumerator.cs b/Projects/Server/Maps/Map.MobileByDistanceEnumerator.cs new file mode 100644 index 000000000..a8d42c693 --- /dev/null +++ b/Projects/Server/Maps/Map.MobileByDistanceEnumerator.cs @@ -0,0 +1,320 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: Map.MobileByDistanceEnumerator.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 Server.Collections; + +namespace Server; + +public partial class Map +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInRangeByDistance(Point3D p) => + GetMobilesInRangeByDistance(p, Core.GlobalMaxUpdateRange); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInRangeByDistance(Point3D p, int range) => + GetMobilesInRangeByDistance(p, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInRangeByDistance(Point3D p) where T : Mobile => + GetMobilesInRangeByDistance(p, Core.GlobalMaxUpdateRange); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInRangeByDistance(Point3D p, int range) where T : Mobile => + GetMobilesInRangeByDistance(p.m_X, p.m_Y, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInRangeByDistance(Point2D p) => + GetMobilesInRangeByDistance(p, Core.GlobalMaxUpdateRange); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInRangeByDistance(Point2D p, int range) => + GetMobilesInRangeByDistance(p, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInRangeByDistance(Point2D p) where T : Mobile => + GetMobilesInRangeByDistance(p, Core.GlobalMaxUpdateRange); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInRangeByDistance(Point2D p, int range) where T : Mobile => + GetMobilesInRangeByDistance(p.m_X, p.m_Y, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInRangeByDistance(int x, int y, int range) => + GetMobilesInRangeByDistance(x, y, range); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInRangeByDistance(int x, int y, int range) where T : Mobile + { + var clampedRange = Math.Max(0, range); + var edge = clampedRange * 2 + 1; + return GetMobilesInBoundsByDistance( + new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge), + new Point2D(x, y) + ); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInBoundsByDistance(Rectangle2D bounds) => + GetMobilesInBoundsByDistance(bounds); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable GetMobilesInBoundsByDistance(Rectangle2D bounds, bool makeBoundsInclusive = false) where T : Mobile => + GetMobilesInBoundsByDistance(bounds, new Point2D(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2), makeBoundsInclusive); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private MobileDistanceEnumerable GetMobilesInBoundsByDistance( + Rectangle2D bounds, Point2D center, bool makeBoundsInclusive = false + ) where T : Mobile => new(this, bounds, center, makeBoundsInclusive); + + public ref struct MobileDistanceEnumerable where T : Mobile + { + private readonly Map _map; + private readonly Rectangle2D _bounds; + private readonly Point2D _center; + private readonly bool _makeBoundsInclusive; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerable(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive) + { + _map = map; + _bounds = bounds; + _center = center; + _makeBoundsInclusive = makeBoundsInclusive; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerator GetEnumerator() => new(_map, _bounds, _center, _makeBoundsInclusive); + } + + public ref struct MobileDistanceEnumerator where T : Mobile + { + private Map _map; + private Point2D _center; + private Rectangle2D _bounds; + + private int _sectorStartX; + private int _maxRing; + + private int _ring; // -1 = uninitialized, then 0.._maxRing + private int _ringIndex; // Current index within the ring + + private int _currentSectorX; + private int _currentSectorY; + + private ref readonly ValueLinkList _linkList; + private int _currentVersion; + private T _current; + + private int _minDistance; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MobileDistanceEnumerator(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive) + { + _map = map; + _center = center; + _bounds = makeBoundsInclusive + ? new Rectangle2D(bounds.X, bounds.Y, bounds.Width + 1, bounds.Height + 1) + : bounds; + + _current = null; + + if (map != null) + { + var centerSectorX = center.m_X / SectorSize; + var centerSectorY = center.m_Y / SectorSize; + + map.CalculateSectors(_bounds, out _sectorStartX, out var sectorStartY, out var sectorEndX, out var sectorEndY); + + // Calculate max ring based on bounds + var dx = Math.Max(centerSectorX - _sectorStartX, sectorEndX - centerSectorX); + var dy = Math.Max(centerSectorY - sectorStartY, sectorEndY - centerSectorY); + _maxRing = Math.Max(dx, dy); + } + + _ring = -1; + _ringIndex = -1; + _currentSectorX = 0; + _currentSectorY = 0; + + _currentVersion = 0; + _minDistance = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + var map = _map; + + if (map == null) + { + return false; + } + + if (!Unsafe.IsNullRef(in _linkList) && _linkList.Version != _currentVersion) + { + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); + } + + Mobile current = _current; + + while (true) + { + current = current?.Next; + + while (current == null) + { + while (!TryNextSectorInRing(out _currentSectorX, out _currentSectorY)) + { + // Current ring exhausted, try next ring + if (_ring >= _maxRing) + { + return false; // No more rings to search + } + + _ring++; + _ringIndex = -1; + } + + _linkList = ref map.GetRealSector(_currentSectorX, _currentSectorY).Mobiles; + _currentVersion = _linkList.Version; + current = _linkList._first; + + if (current != null) + { + _minDistance = MinDistSqToSectorRect(_center.m_X, _center.m_Y, _currentSectorX, _currentSectorY); + } + } + + if (current is T { Deleted: false } o && _bounds.Contains(o.Location)) + { + _current = o; + return true; + } + } + } + + public (T Value, int MinDistance) Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (_current, _minDistance); + } + private bool TryNextSectorInRing(out int sx, out int sy) + { + if (_ring == 0) + { + // Center sector + if (_ringIndex < 0) + { + _ringIndex = 0; + sx = _center.m_X / SectorSize; + sy = _center.m_Y / SectorSize; + return sx >= _sectorStartX; + } + + sx = sy = 0; + return false; + } + + var totalSectors = _ring * 8; + + // Keep trying sectors in this ring until we find a valid one or exhaust the ring + while (true) + { + var nextIndex = _ringIndex + 1; + + if (nextIndex >= totalSectors) + { + sx = sy = 0; + return false; + } + + _ringIndex = nextIndex; + CalculatePositionFromIndex(nextIndex, out sx, out sy); + + if (sx >= _sectorStartX) + { + return true; + } + } + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CalculatePositionFromIndex(int index, out int x, out int y) + { + var centerSectorX = _center.m_X / SectorSize; + var centerSectorY = _center.m_Y / SectorSize; + + var ringSize = _ring * 2; + var startX = centerSectorX - _ring; + var startY = centerSectorY - _ring; + + if (index <= ringSize) // Top edge + { + x = startX + index; + y = startY; + } + else if (index <= ringSize * 2) // Right edge + { + x = startX + ringSize; + y = startY + (index - ringSize); + } + else if (index <= ringSize * 3) // Bottom edge + { + x = startX + ringSize - (index - ringSize * 2); + y = startY + ringSize; + } + else // Left edge + { + x = startX; + y = startY + ringSize - (index - ringSize * 3); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int MinDistSqToSectorRect(int cx, int cy, int sectorX, int sectorY) + { + var x0 = sectorX * SectorSize; + var y0 = sectorY * SectorSize; + var x1 = x0 + (SectorSize - 1); + var y1 = y0 + (SectorSize - 1); + + var dx = 0; + if (cx < x0) + { + dx = x0 - cx; + } + else if (cx > x1) + { + dx = cx - x1; + } + + var dy = 0; + if (cy < y0) + { + dy = y0 - cy; + } + else if (cy > y1) + { + dy = cy - y1; + } + + return dx * dx + dy * dy; + } + } +} diff --git a/Projects/Server/Maps/Map.MobileEnumerator.cs b/Projects/Server/Maps/Map.MobileEnumerator.cs index cac64ce3a..01def1015 100644 --- a/Projects/Server/Maps/Map.MobileEnumerator.cs +++ b/Projects/Server/Maps/Map.MobileEnumerator.cs @@ -69,8 +69,12 @@ public partial class Map GetMobilesInRange(p.m_X, p.m_Y, range); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public MobileBoundsEnumerable GetMobilesInRange(int x, int y, int range) where T : Mobile => - GetMobilesInBounds(new Rectangle2D(x - range, y - range, range * 2 + 1, range * 2 + 1)); + public MobileBoundsEnumerable GetMobilesInRange(int x, int y, int range) where T : Mobile + { + var clampedRange = Math.Max(0, range); + var edge = clampedRange * 2 + 1; + return GetMobilesInBounds(new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge)); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public MobileBoundsEnumerable GetMobilesInBounds(Rectangle2D bounds) => GetMobilesInBounds(bounds); diff --git a/Projects/Server/Maps/Map.MultiEnumerator.cs b/Projects/Server/Maps/Map.MultiEnumerator.cs index 807e351b1..2069d4c94 100644 --- a/Projects/Server/Maps/Map.MultiEnumerator.cs +++ b/Projects/Server/Maps/Map.MultiEnumerator.cs @@ -70,8 +70,12 @@ public partial class Map GetMultisInRange(p.m_X, p.m_Y, range); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public MultiBoundsEnumerable GetMultisInRange(int x, int y, int range) where T : BaseMulti => - GetMultisInBounds(new Rectangle2D(x - range, y - range, range * 2 + 1, range * 2 + 1)); + public MultiBoundsEnumerable GetMultisInRange(int x, int y, int range) where T : BaseMulti + { + var clampedRange = Math.Max(0, range); + var edge = clampedRange * 2 + 1; + return GetMultisInBounds(new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge)); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public MultiBoundsEnumerable GetMultisInBounds(Rectangle2D bounds, bool makeBoundsInclusive = false) =>