diff --git a/Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs new file mode 100644 index 000000000..68a867728 --- /dev/null +++ b/Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs @@ -0,0 +1,492 @@ +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using Server.Accounting; +using Server.Network; +using Xunit; + +namespace Server.Tests.Tests.Maps; + +[Collection("Sequential Server Tests")] +public class ClientEnumeratorTests +{ + [Fact] + public void ClientEnumerator_FiltersByBoundsAndOrder() + { + var map = Map.Felucca; + var rect = new Rectangle2D(100, 100, 32, 32); + + var clients = new (NetState, Mobile)[3]; + try + { + clients[0] = CreateClientWithMobile(map, new Point3D(105, 105, 0)); + clients[1] = CreateClientWithMobile(map, new Point3D(130, 130, 0)); + clients[2] = CreateClientWithMobile(map, new Point3D(90, 90, 0)); + + var found = new List(); + foreach (var ns in map.GetClientsInBounds(rect)) + { + found.Add(ns); + } + + Assert.Equal(2, found.Count); + Assert.All(found, ns => Assert.True(rect.Contains(ns.Mobile.Location))); + Assert.Equal(new[] { clients[0].Item1, clients[1].Item1 }, found); + } + finally + { + DeleteAll(clients); + } + } + + [Fact] + public void ClientEnumerator_SkipsNullMobiles() + { + var map = Map.Felucca; + var rect = new Rectangle2D(200, 200, 16, 16); + + var clients = new (NetState, Mobile)[3]; + try + { + clients[0] = CreateClientWithMobile(map, new Point3D(205, 205, 0)); + clients[1] = CreateClientWithMobile(map, new Point3D(206, 205, 0)); + clients[2] = CreateClientWithMobile(map, new Point3D(207, 205, 0)); + + // Remove the mobile from the second client + clients[1].Item2.Delete(); + clients[1].Item1.Mobile = null; + + var found = new List(); + foreach (var ns in map.GetClientsInBounds(rect)) + { + found.Add(ns); + } + + Assert.Equal(2, found.Count); + Assert.Equal(new[] { clients[0].Item1, clients[2].Item1 }, found); + } + finally + { + DeleteAll(clients); + } + } + + [Fact] + public void ClientEnumerator_RespectsMakeBoundsInclusiveFlag() + { + var map = Map.Felucca; + var rect = new Rectangle2D(300, 300, 1, 1); + + var clients = new (NetState, Mobile)[1]; + try + { + clients[0] = CreateClientWithMobile(map, new Point3D(301, 301, 0)); + + var enumerator = map.GetClientsInBounds(rect, makeBoundsInclusive: true).GetEnumerator(); + + Assert.True(enumerator.MoveNext()); + Assert.Equal(clients[0].Item1, enumerator.Current); + } + finally + { + DeleteAll(clients); + } + } + + [Fact] + public void ClientEnumerator_MapNullYieldsEmpty() + { + var enumerator = new Map.ClientBoundsEnumerable(null, Rectangle2D.Empty, false).GetEnumerator(); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void ClientEnumerator_ThrowsOnVersionChange() + { + var map = Map.Felucca; + var rect = new Rectangle2D(400, 400, 16, 16); + + var clients = new[] + { + CreateClientWithMobile(map, new Point3D(405, 405, 0)), + CreateClientWithMobile(map, new Point3D(406, 405, 0)) + }; + + try + { + var enumerator = map.GetClientsInBounds(rect).GetEnumerator(); + Assert.True(enumerator.MoveNext()); + + clients[1].Item2.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(clients); + } + } + + [Fact] + public void ClientEnumerator_StepsAcrossSectors() + { + var map = Map.Felucca; + var rect = new Rectangle2D(500, 500, Map.SectorSize * 2, Map.SectorSize * 2); + + var clients = new[] + { + CreateClientWithMobile(map, new Point3D(rect.X + 1, rect.Y + 1, 0)), + CreateClientWithMobile(map, new Point3D(rect.X + Map.SectorSize + 1, rect.Y + 1, 0)), + CreateClientWithMobile(map, new Point3D(rect.X + Map.SectorSize + 1, rect.Y + Map.SectorSize + 1, 0)) + }; + + try + { + var result = new List(); + foreach (var ns in map.GetClientsInBounds(rect)) + { + result.Add(ns); + } + + Assert.Equal(new[] { clients[0].Item1, clients[1].Item1, clients[2].Item1 }, result); + } + finally + { + DeleteAll(clients); + } + } + + [Fact] + public void ClientEnumerator_MapBoundsAreClamped() + { + var map = Map.Felucca; + var width = map.Width; + var height = map.Height; + + var rect = new Rectangle2D(width - Map.SectorSize - 2, height - Map.SectorSize - 2, Map.SectorSize * 2, Map.SectorSize * 2); + + var clients = new[] + { + CreateClientWithMobile(map, new Point3D(width - 2, height - 2, 0)) + }; + + try + { + var enumerator = map.GetClientsInBounds(rect).GetEnumerator(); + + Assert.True(enumerator.MoveNext()); + Assert.Equal(clients[0].Item1, enumerator.Current); + Assert.False(enumerator.MoveNext()); + } + finally + { + DeleteAll(clients); + } + } + + [Fact] + public void ClientAtEnumerator_FiltersExactLocation() + { + var map = Map.Felucca; + var location = new Point3D(600, 600, 0); + var differentLocation = new Point3D(601, 600, 0); + + var clients = new (NetState, Mobile)[3]; + try + { + clients[0] = CreateClientWithMobile(map, location); + clients[1] = CreateClientWithMobile(map, location); + clients[2] = CreateClientWithMobile(map, differentLocation); + + var found = new List(); + foreach (var ns in map.GetClientsAt(location)) + { + found.Add(ns); + } + + Assert.Equal(2, found.Count); + Assert.All(found, ns => + { + Assert.NotNull(ns.Mobile); + Assert.Equal(location.X, ns.Mobile.X); + Assert.Equal(location.Y, ns.Mobile.Y); + }); + Assert.Contains(clients[0].Item1, found); + Assert.Contains(clients[1].Item1, found); + } + finally + { + DeleteAll(clients); + } + } + + [Fact] + public void ClientAtEnumerator_SkipsNullMobiles() + { + var map = Map.Felucca; + var location = new Point3D(650, 650, 0); + + var clients = new (NetState, Mobile)[3]; + try + { + clients[0] = CreateClientWithMobile(map, location); + clients[1] = CreateClientWithMobile(map, location); + clients[2] = CreateClientWithMobile(map, location); + + // Remove the mobile from the second client + clients[1].Item2.Delete(); + clients[1].Item1.Mobile = null; + + var found = new List(); + foreach (var ns in map.GetClientsAt(location)) + { + found.Add(ns); + } + + Assert.Equal(2, found.Count); + Assert.Equal(new[] { clients[0].Item1, clients[2].Item1 }, found); + } + finally + { + DeleteAll(clients); + } + } + + [Fact] + public void ClientAtEnumerator_MapNullYieldsEmpty() + { + var enumerator = new Map.ClientAtEnumerable(null, new Point2D(0, 0)).GetEnumerator(); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void ClientAtEnumerator_ThrowsOnVersionChange() + { + var map = Map.Felucca; + var location = new Point3D(750, 750, 0); + + var clients = new[] + { + CreateClientWithMobile(map, location), + CreateClientWithMobile(map, location) + }; + + try + { + var enumerator = map.GetClientsAt(location).GetEnumerator(); + Assert.True(enumerator.MoveNext()); + + clients[1].Item2.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(clients); + } + } + + [Fact] + public void ClientAtEnumerator_UsesDifferentPoint3DOverloads() + { + var map = Map.Felucca; + var location = new Point3D(800, 800, 5); + + var clients = new (NetState, Mobile)[1]; + try + { + clients[0] = CreateClientWithMobile(map, location); + + // Test Point3D overload + var found1 = new List(); + foreach (var ns in map.GetClientsAt(location)) + { + found1.Add(ns); + } + + // Test (int, int) overload - should find the same client (Z is ignored) + var found2 = new List(); + foreach (var ns in map.GetClientsAt(location.X, location.Y)) + { + found2.Add(ns); + } + + // Test Point2D overload + var found3 = new List(); + foreach (var ns in map.GetClientsAt(new Point2D(location.X, location.Y))) + { + found3.Add(ns); + } + + Assert.Single(found1); + Assert.Equal(clients[0].Item1, found1[0]); + Assert.Equal(found1, found2); + Assert.Equal(found1, found3); + } + finally + { + DeleteAll(clients); + } + } + + [Fact] + public void ClientEnumerator_GetClientsInRange() + { + var map = Map.Felucca; + var center = new Point3D(900, 900, 0); + var range = 5; + + var clients = new (NetState, Mobile)[3]; + try + { + clients[0] = CreateClientWithMobile(map, new Point3D(902, 902, 0)); // Within range + clients[1] = CreateClientWithMobile(map, new Point3D(898, 898, 0)); // Within range + clients[2] = CreateClientWithMobile(map, new Point3D(910, 910, 0)); // Outside range (906+ is outside) + + var found = new List(); + foreach (var ns in map.GetClientsInRange(center, range)) + { + found.Add(ns); + } + + // GetClientsInRange uses a bounding rectangle, not circular distance + // Range of 5 means rectangle from (895, 895) to (905, 905) + Assert.Equal(2, found.Count); + Assert.Contains(clients[0].Item1, found); + Assert.Contains(clients[1].Item1, found); + } + finally + { + DeleteAll(clients); + } + } + + [Fact] + public void ClientEnumerator_DeletedMobilesAreSkipped() + { + var map = Map.Felucca; + var rect = new Rectangle2D(1000, 1000, 16, 16); + + var clients = new (NetState, Mobile)[3]; + try + { + clients[0] = CreateClientWithMobile(map, new Point3D(1005, 1005, 0)); + clients[1] = CreateClientWithMobile(map, new Point3D(1006, 1005, 0)); + clients[2] = CreateClientWithMobile(map, new Point3D(1007, 1005, 0)); + + // Delete the mobile (but not the NetState) + clients[1].Item2.Delete(); + + var found = new List(); + foreach (var ns in map.GetClientsInBounds(rect)) + { + found.Add(ns); + } + + // Should skip the client whose mobile was deleted + Assert.Equal(2, found.Count); + Assert.Contains(clients[0].Item1, found); + Assert.Contains(clients[2].Item1, found); + } + finally + { + DeleteAll(clients); + } + } + + private class MockAccount : IAccount + { + public int TotalGold { get; } + public int TotalPlat { get; } + public bool DepositGold(int amount) => throw new NotImplementedException(); + public bool DepositPlat(int amount) => throw new NotImplementedException(); + public bool WithdrawGold(int amount) => throw new NotImplementedException(); + public bool WithdrawPlat(int amount) => throw new NotImplementedException(); + public long GetTotalGold() => throw new NotImplementedException(); + public int CompareTo(IAccount other) => throw new NotImplementedException(); + public string Username { get; } + public string Email { get; set; } + public AccessLevel AccessLevel { get; set; } + public int Length { get; } + public int Limit { get; set; } = 6; // Default to 6 character slots + public int Count { get; } + + private readonly Dictionary _mobiles = new(); + public Mobile this[int index] + { + get => _mobiles.GetValueOrDefault(index); + set => _mobiles[index] = value; + } + + public DateTime Created { get; set; } + public Serial Serial { get; } + public void Deserialize(IGenericReader reader) => throw new NotImplementedException(); + public byte SerializedThread { get; set; } + public int SerializedPosition { get; set; } + public int SerializedLength { get; set; } + public void Serialize(IGenericWriter writer) => throw new NotImplementedException(); + public bool Deleted { get; } + public void Delete() => throw new NotImplementedException(); + public bool TrySetUsername(string username) => throw new NotImplementedException(); + public void SetPassword(string password) => throw new NotImplementedException(); + public bool CheckPassword(string password) => throw new NotImplementedException(); + } + + private static (NetState, Mobile) CreateClientWithMobile(Map map, Point3D location) + { + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + var ns = new NetState(socket); + + // Assign a mock account to avoid null reference issues + ns.Account = new MockAccount(); + + // Use a unique serial for each mobile + var serial = World.NewMobile; + var mobile = new Mobile(serial); + mobile.DefaultMobileInit(); + + // Set the NetState on the mobile BEFORE moving it to the world + // so the sector's client list gets updated properly + ns.Mobile = mobile; + mobile.NetState = ns; + + mobile.MoveToWorld(location, map); + return (ns, mobile); + } + + private static void DeleteAll((NetState, Mobile)[] clients) + { + for (var i = 0; i < clients.Length; i++) + { + if (clients[i].Item1 != null) + { + clients[i].Item1.Mobile = null; + clients[i].Item1.Disconnect("Test cleanup"); + } + clients[i].Item2?.Delete(); + } + } +} + diff --git a/Projects/Server.Tests/Tests/Maps/ItemEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/ItemEnumeratorTests.cs new file mode 100644 index 000000000..ef37ade80 --- /dev/null +++ b/Projects/Server.Tests/Tests/Maps/ItemEnumeratorTests.cs @@ -0,0 +1,427 @@ +using System; +using System.Collections.Generic; +using Server.Items; +using Xunit; + +namespace Server.Tests.Tests.Maps; + +[Collection("Sequential Server Tests")] +public class ItemEnumeratorTests +{ + [Fact] + public void ItemEnumerator_FiltersByBoundsAndOrder() + { + var map = Map.Felucca; + var rect = new Rectangle2D(100, 100, 32, 32); + + var items = new Item[3]; + try + { + items[0] = CreateItem(map, new Point3D(105, 105, 0)); + items[1] = CreateItem(map, new Point3D(130, 130, 0)); + items[2] = CreateItem(map, new Point3D(90, 90, 0)); + + var found = new List(); + foreach (var item in map.GetItemsInBounds(rect)) + { + found.Add(item); + } + + Assert.Equal(2, found.Count); + Assert.All(found, item => Assert.True(rect.Contains(item.Location))); + Assert.Equal(new[] { items[0], items[1] }, found); + } + finally + { + DeleteAll(items); + } + } + + [Fact] + public void ItemEnumerator_DeletedItemsAreSkipped() + { + var map = Map.Felucca; + var rect = new Rectangle2D(200, 200, 16, 16); + + var items = new Item[3]; + try + { + items[0] = CreateItem(map, new Point3D(205, 205, 0)); + items[1] = CreateItem(map, new Point3D(206, 205, 0)); + items[2] = CreateItem(map, new Point3D(207, 205, 0)); + + items[1].Delete(); + + var found = new List(); + foreach (var item in map.GetItemsInBounds(rect)) + { + found.Add(item); + } + + Assert.Equal(new[] { items[0], items[2] }, found); + } + finally + { + DeleteAll(items); + } + } + + [Fact] + public void ItemEnumerator_ItemsWithParentAreSkipped() + { + var map = Map.Felucca; + var rect = new Rectangle2D(250, 250, 16, 16); + + var items = new Item[2]; + var container = new Container(0xE75); + try + { + items[0] = CreateItem(map, new Point3D(255, 255, 0)); + items[1] = CreateItem(map, new Point3D(256, 255, 0)); + container.MoveToWorld(new Point3D(255, 255, 0), map); + + // Move items[1] into the container - it should be skipped + items[1].Parent = container; + + var found = new List(); + foreach (var item in map.GetItemsInBounds(rect)) + { + found.Add(item); + } + + // Should only find items[0] and container, not items[1] (which has a parent) + Assert.Equal(2, found.Count); + Assert.Contains(items[0], found); + Assert.Contains(container, found); + Assert.DoesNotContain(items[1], found); + } + finally + { + DeleteAll(items); + container?.Delete(); + } + } + + [Fact] + public void ItemEnumerator_RespectsMakeBoundsInclusiveFlag() + { + var map = Map.Felucca; + var rect = new Rectangle2D(300, 300, 1, 1); + + var items = new Item[1]; + try + { + items[0] = CreateItem(map, new Point3D(301, 301, 0)); + + var enumerator = map.GetItemsInBounds(rect, makeBoundsInclusive: true).GetEnumerator(); + + Assert.True(enumerator.MoveNext()); + Assert.Equal(items[0], enumerator.Current); + } + finally + { + DeleteAll(items); + } + } + + [Fact] + public void ItemEnumerator_MapNullYieldsEmpty() + { + var enumerator = new Map.ItemEnumerator(null, Rectangle2D.Empty, false); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void ItemEnumerator_ThrowsOnVersionChange() + { + var map = Map.Felucca; + var rect = new Rectangle2D(400, 400, 16, 16); + + var items = new[] + { + CreateItem(map, new Point3D(405, 405, 0)), + CreateItem(map, new Point3D(406, 405, 0)) + }; + + try + { + var enumerator = map.GetItemsInBounds(rect).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 ItemEnumerator_StepsAcrossSectors() + { + var map = Map.Felucca; + var rect = new Rectangle2D(500, 500, Map.SectorSize * 2, Map.SectorSize * 2); + + var items = new[] + { + CreateItem(map, new Point3D(rect.X + 1, rect.Y + 1, 0)), + CreateItem(map, new Point3D(rect.X + Map.SectorSize + 1, rect.Y + 1, 0)), + CreateItem(map, new Point3D(rect.X + Map.SectorSize + 1, rect.Y + Map.SectorSize + 1, 0)) + }; + + try + { + var result = new List(); + foreach (var item in map.GetItemsInBounds(rect)) + { + result.Add(item); + } + + Assert.Equal(items, result); + } + finally + { + DeleteAll(items); + } + } + + [Fact] + public void ItemEnumerator_MapBoundsAreClamped() + { + var map = Map.Felucca; + var width = map.Width; + var height = map.Height; + + var rect = new Rectangle2D(width - Map.SectorSize - 2, height - Map.SectorSize - 2, Map.SectorSize * 2, Map.SectorSize * 2); + + var items = new[] + { + CreateItem(map, new Point3D(width - 2, height - 2, 0)) + }; + + try + { + var enumerator = map.GetItemsInBounds(rect).GetEnumerator(); + + Assert.True(enumerator.MoveNext()); + Assert.Equal(items[0], enumerator.Current); + Assert.False(enumerator.MoveNext()); + } + finally + { + DeleteAll(items); + } + } + + [Fact] + public void ItemAtEnumerator_FiltersExactLocation() + { + var map = Map.Felucca; + var location = new Point3D(600, 600, 0); + + var items = new Item[3]; + try + { + items[0] = CreateItem(map, location); + items[1] = CreateItem(map, location); + items[2] = CreateItem(map, new Point3D(601, 600, 0)); // Different location + + var found = new List(); + foreach (var item in map.GetItemsAt(location)) + { + 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 ItemAtEnumerator_DeletedItemsAreSkipped() + { + var map = Map.Felucca; + var location = new Point3D(650, 650, 0); + + var items = new Item[3]; + try + { + items[0] = CreateItem(map, location); + items[1] = CreateItem(map, location); + items[2] = CreateItem(map, location); + + items[1].Delete(); + + var found = new List(); + foreach (var item in map.GetItemsAt(location)) + { + found.Add(item); + } + + Assert.Equal(new[] { items[0], items[2] }, found); + } + finally + { + DeleteAll(items); + } + } + + [Fact] + public void ItemAtEnumerator_ItemsWithParentAreSkipped() + { + var map = Map.Felucca; + var location = new Point3D(700, 700, 0); + + var items = new Item[2]; + var container = new Container(0xE75); + try + { + items[0] = CreateItem(map, location); + items[1] = CreateItem(map, location); + container.MoveToWorld(location, map); + + // Move items[1] into the container - it should be skipped + items[1].Parent = container; + + var found = new List(); + foreach (var item in map.GetItemsAt(location)) + { + found.Add(item); + } + + Assert.Equal(2, found.Count); + Assert.Contains(items[0], found); + Assert.Contains(container, found); + Assert.DoesNotContain(items[1], found); + } + finally + { + DeleteAll(items); + container?.Delete(); + } + } + + [Fact] + public void ItemAtEnumerator_MapNullYieldsEmpty() + { + var enumerator = new Map.ItemAtEnumerator(null, new Point2D(0, 0)); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void ItemAtEnumerator_ThrowsOnVersionChange() + { + var map = Map.Felucca; + var location = new Point3D(750, 750, 0); + + var items = new[] + { + CreateItem(map, location), + CreateItem(map, location) + }; + + try + { + var enumerator = map.GetItemsAt(location).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 ItemAtEnumerator_UsesDifferentPoint3DOverloads() + { + var map = Map.Felucca; + var location = new Point3D(800, 800, 5); + + var items = new Item[1]; + try + { + items[0] = CreateItem(map, location); + + // Test Point3D overload + var found1 = new List(); + foreach (var item in map.GetItemsAt(location)) + { + found1.Add(item); + } + + // Test (int, int) overload - should find the same item (Z is ignored) + var found2 = new List(); + foreach (var item in map.GetItemsAt(location.X, location.Y)) + { + found2.Add(item); + } + + // Test Point2D overload + var found3 = new List(); + foreach (var item in map.GetItemsAt(new Point2D(location.X, location.Y))) + { + found3.Add(item); + } + + Assert.Single(found1); + Assert.Equal(items[0], found1[0]); + Assert.Equal(found1, found2); + Assert.Equal(found1, found3); + } + finally + { + DeleteAll(items); + } + } + + private static Item CreateItem(Map map, Point3D location) + { + var item = new Item(0x1); + item.Movable = false; + item.MoveToWorld(location, map); + return item; + } + + private static void DeleteAll(Item[] items) + { + for (var i = 0; i < items.Length; i++) + { + items[i]?.Delete(); + } + } +} + diff --git a/Projects/Server.Tests/Tests/Maps/MobileEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/MobileEnumeratorTests.cs new file mode 100644 index 000000000..5c7786a92 --- /dev/null +++ b/Projects/Server.Tests/Tests/Maps/MobileEnumeratorTests.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace Server.Tests.Tests.Maps; + +[Collection("Sequential Server Tests")] +public class MobileEnumeratorTests +{ + [Fact] + public void MobileEnumerator_FiltersByBoundsAndOrder() + { + var map = Map.Felucca; + var rect = new Rectangle2D(100, 100, 32, 32); + + var mobiles = new Mobile[3]; + try + { + mobiles[0] = CreateMobile(map, new Point3D(105, 105, 0)); + mobiles[1] = CreateMobile(map, new Point3D(130, 130, 0)); + mobiles[2] = CreateMobile(map, new Point3D(90, 90, 0)); + + var found = new List(); + foreach (var m in map.GetMobilesInBounds(rect)) + { + found.Add(m); + } + + Assert.Equal(2, found.Count); + Assert.All(found, m => Assert.True(rect.Contains(m.Location))); + Assert.Equal(new[] { mobiles[0], mobiles[1] }, found); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileEnumerator_DeletedMobilesAreSkipped() + { + var map = Map.Felucca; + var rect = new Rectangle2D(200, 200, 16, 16); + + var mobiles = new Mobile[3]; + try + { + mobiles[0] = CreateMobile(map, new Point3D(205, 205, 0)); + mobiles[1] = CreateMobile(map, new Point3D(206, 205, 0)); + mobiles[2] = CreateMobile(map, new Point3D(207, 205, 0)); + + mobiles[1].Delete(); + + var found = new List(); + foreach (var m in map.GetMobilesInBounds(rect)) + { + found.Add(m); + } + + Assert.Equal(new[] { mobiles[0], mobiles[2] }, found); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileEnumerator_RespectsMakeBoundsInclusiveFlag() + { + var map = Map.Felucca; + var rect = new Rectangle2D(300, 300, 1, 1); + + var mobiles = new Mobile[1]; + try + { + mobiles[0] = CreateMobile(map, new Point3D(301, 301, 0)); + + var enumerator = map.GetMobilesInBounds(rect, makeBoundsInclusive: true).GetEnumerator(); + + Assert.True(enumerator.MoveNext()); + Assert.Equal(mobiles[0], enumerator.Current); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileEnumerator_MapNullYieldsEmpty() + { + var enumerator = new Map.MobileEnumerator(null, Rectangle2D.Empty, false); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void MobileEnumerator_ThrowsOnVersionChange() + { + var map = Map.Felucca; + var rect = new Rectangle2D(400, 400, 16, 16); + + var mobiles = new[] + { + CreateMobile(map, new Point3D(405, 405, 0)), + CreateMobile(map, new Point3D(406, 405, 0)) + }; + + try + { + var enumerator = map.GetMobilesInBounds(rect).GetEnumerator(); + Assert.True(enumerator.MoveNext()); + + mobiles[1].Delete(); + + 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 MobileEnumerator_StepsAcrossSectors() + { + var map = Map.Felucca; + var rect = new Rectangle2D(500, 500, Map.SectorSize * 2, Map.SectorSize * 2); + + var mobiles = new[] + { + CreateMobile(map, new Point3D(rect.X + 1, rect.Y + 1, 0)), + CreateMobile(map, new Point3D(rect.X + Map.SectorSize + 1, rect.Y + 1, 0)), + CreateMobile(map, new Point3D(rect.X + Map.SectorSize + 1, rect.Y + Map.SectorSize + 1, 0)) + }; + + try + { + var result = new List(); + foreach (var m in map.GetMobilesInBounds(rect)) + { + result.Add(m); + } + + Assert.Equal(mobiles, result); + } + finally + { + DeleteAll(mobiles); + } + } + + [Fact] + public void MobileEnumerator_MapBoundsAreClamped() + { + var map = Map.Felucca; + var width = map.Width; + var height = map.Height; + + var rect = new Rectangle2D(width - Map.SectorSize - 2, height - Map.SectorSize - 2, Map.SectorSize * 2, Map.SectorSize * 2); + + var mobiles = new[] + { + CreateMobile(map, new Point3D(width - 2, height - 2, 0)) + }; + + try + { + var enumerator = map.GetMobilesInBounds(rect).GetEnumerator(); + + Assert.True(enumerator.MoveNext()); + Assert.Equal(mobiles[0], enumerator.Current); + Assert.False(enumerator.MoveNext()); + } + finally + { + DeleteAll(mobiles); + } + } + + private static Mobile CreateMobile(Map map, Point3D location) + { + var mobile = new Mobile((Serial)Utility.RandomMinMax(0x100u, 0xFFFu)); + mobile.DefaultMobileInit(); + mobile.MoveToWorld(location, map); + return mobile; + } + + private static void DeleteAll(Mobile[] mobiles) + { + for (var i = 0; i < mobiles.Length; i++) + { + mobiles[i]?.Delete(); + } + } +} diff --git a/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs new file mode 100644 index 000000000..0f592ca83 --- /dev/null +++ b/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections.Generic; +using Server.Items; +using Xunit; + +namespace Server.Tests.Tests.Maps; + +[Collection("Sequential Server Tests")] +public class MultiEnumeratorTests +{ + [Fact] + public void MultiEnumerator_FiltersByBoundsAndOrder() + { + var map = Map.Felucca; + var rect = new Rectangle2D(100, 100, 32, 32); + + var multis = new TestMulti[3]; + try + { + multis[0] = CreateMulti(map, new Point3D(105, 105, 0)); + multis[1] = CreateMulti(map, new Point3D(130, 130, 0)); + multis[2] = CreateMulti(map, new Point3D(90, 90, 0)); + + var found = new List(); + foreach (var multi in map.GetMultisInBounds(rect)) + { + found.Add(multi); + } + + Assert.Equal(2, found.Count); + Assert.All(found, multi => Assert.True(rect.Contains(multi.Location))); + Assert.Equal(new[] { multis[0], multis[1] }, found); + } + finally + { + DeleteAll(multis); + } + } + + [Fact] + public void MultiEnumerator_DeletedMultisAreSkipped() + { + var map = Map.Felucca; + var rect = new Rectangle2D(200, 200, 16, 16); + + var multis = new TestMulti[3]; + try + { + multis[0] = CreateMulti(map, new Point3D(205, 205, 0)); + multis[1] = CreateMulti(map, new Point3D(206, 205, 0)); + multis[2] = CreateMulti(map, new Point3D(207, 205, 0)); + + multis[1].Delete(); + + var found = new List(); + foreach (var multi in map.GetMultisInBounds(rect)) + { + found.Add(multi); + } + + Assert.Equal(new[] { multis[0], multis[2] }, found); + } + finally + { + DeleteAll(multis); + } + } + + [Fact] + public void MultiEnumerator_RespectsMakeBoundsInclusiveFlag() + { + var map = Map.Felucca; + var rect = new Rectangle2D(300, 300, 1, 1); + + var multis = new TestMulti[1]; + try + { + multis[0] = CreateMulti(map, new Point3D(301, 301, 0)); + + var enumerator = map.GetMultisInBounds(rect, makeBoundsInclusive: true).GetEnumerator(); + + Assert.True(enumerator.MoveNext()); + Assert.Equal(multis[0], enumerator.Current); + } + finally + { + DeleteAll(multis); + } + } + + [Fact] + public void MultiEnumerator_MapNullYieldsEmpty() + { + var enumerator = new Map.MultiBoundsEnumerable(null, Rectangle2D.Empty, false).GetEnumerator(); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void MultiEnumerator_ThrowsOnVersionChange() + { + var map = Map.Felucca; + var rect = new Rectangle2D(400, 400, 16, 16); + + var multis = new[] + { + CreateMulti(map, new Point3D(405, 405, 0)), + CreateMulti(map, new Point3D(406, 405, 0)) + }; + + try + { + var enumerator = map.GetMultisInBounds(rect).GetEnumerator(); + Assert.True(enumerator.MoveNext()); + + multis[1].Delete(); + + // Ref structs cannot be captured in lambdas, so we test the exception directly + var exceptionThrown = false; + try + { + enumerator.MoveNext(); + } + catch (Exception e) + { + Assert.IsType(e); + exceptionThrown = true; + } + + Assert.True(exceptionThrown, "Expected InvalidOperationException when collection version changes"); + } + finally + { + DeleteAll(multis); + } + } + + [Fact] + public void MultiEnumerator_StepsAcrossSectors() + { + var map = Map.Felucca; + var rect = new Rectangle2D(500, 500, Map.SectorSize * 2, Map.SectorSize * 2); + + var multis = new[] + { + CreateMulti(map, new Point3D(rect.X + 1, rect.Y + 1, 0)), + CreateMulti(map, new Point3D(rect.X + Map.SectorSize + 1, rect.Y + 1, 0)), + CreateMulti(map, new Point3D(rect.X + Map.SectorSize + 1, rect.Y + Map.SectorSize + 1, 0)) + }; + + try + { + var result = new List(); + foreach (var multi in map.GetMultisInBounds(rect)) + { + result.Add(multi); + } + + Assert.Equal(multis, result); + } + finally + { + DeleteAll(multis); + } + } + + [Fact] + public void MultiEnumerator_MapBoundsAreClamped() + { + var map = Map.Felucca; + var width = map.Width; + var height = map.Height; + + var rect = new Rectangle2D(width - Map.SectorSize - 2, height - Map.SectorSize - 2, Map.SectorSize * 2, Map.SectorSize * 2); + + var multis = new[] + { + CreateMulti(map, new Point3D(width - 2, height - 2, 0)) + }; + + try + { + var enumerator = map.GetMultisInBounds(rect).GetEnumerator(); + + Assert.True(enumerator.MoveNext()); + Assert.Equal(multis[0], enumerator.Current); + Assert.False(enumerator.MoveNext()); + } + finally + { + DeleteAll(multis); + } + } + + [Fact] + public void MultiEnumerator_GetMultisInRange() + { + var map = Map.Felucca; + var center = new Point3D(600, 600, 0); + var range = 5; + + var multis = new TestMulti[3]; + try + { + multis[0] = CreateMulti(map, new Point3D(602, 602, 0)); // Within range + multis[1] = CreateMulti(map, new Point3D(598, 598, 0)); // Within range + multis[2] = CreateMulti(map, new Point3D(610, 610, 0)); // Outside range + + var found = new List(); + foreach (var multi in map.GetMultisInRange(center, range)) + { + found.Add(multi); + } + + Assert.Equal(2, found.Count); + Assert.Contains(multis[0], found); + Assert.Contains(multis[1], found); + Assert.DoesNotContain(multis[2], found); + } + finally + { + DeleteAll(multis); + } + } + + [Fact] + public void MultiSectorEnumerator_FiltersToSingleSector() + { + var map = Map.Felucca; + var location = new Point3D(700, 700, 0); + + var multis = new TestMulti[2]; + try + { + multis[0] = CreateMulti(map, location); + multis[1] = CreateMulti(map, new Point3D(location.X + 1, location.Y, 0)); + + var found = new List(); + foreach (var multi in map.GetMultisInSector(location)) + { + found.Add(multi); + } + + // Both should be in the same sector + Assert.Equal(2, found.Count); + Assert.Contains(multis[0], found); + Assert.Contains(multis[1], found); + } + finally + { + DeleteAll(multis); + } + } + + [Fact] + public void MultiSectorEnumerator_DeletedMultisAreSkipped() + { + var map = Map.Felucca; + var location = new Point3D(750, 750, 0); + + var multis = new TestMulti[3]; + try + { + multis[0] = CreateMulti(map, location); + multis[1] = CreateMulti(map, new Point3D(location.X + 1, location.Y, 0)); + multis[2] = CreateMulti(map, new Point3D(location.X + 2, location.Y, 0)); + + multis[1].Delete(); + + var found = new List(); + foreach (var multi in map.GetMultisInRange(location, 10)) + { + found.Add(multi); + } + + Assert.Equal(new[] { multis[0], multis[2] }, found); + } + finally + { + DeleteAll(multis); + } + } + + [Fact] + public void MultiSectorEnumerator_MapNullYieldsEmpty() + { + var enumerator = new Map.MultiSectorEnumerable(null, new Point2D(0, 0)).GetEnumerator(); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void MultiSectorEnumerator_UsesDifferentPointOverloads() + { + var map = Map.Felucca; + var location = new Point3D(800, 800, 5); + + var multis = new TestMulti[1]; + try + { + multis[0] = CreateMulti(map, location); + + // Test Point3D overload + var found1 = new List(); + foreach (var multi in map.GetMultisInSector(location)) + { + found1.Add(multi); + } + + // Test (int, int) overload + var found2 = new List(); + foreach (var multi in map.GetMultisInSector(location.X, location.Y)) + { + found2.Add(multi); + } + + // Test Point2D overload + var found3 = new List(); + foreach (var multi in map.GetMultisInSector(new Point2D(location.X, location.Y))) + { + found3.Add(multi); + } + + Assert.Single(found1); + Assert.Equal(multis[0], found1[0]); + Assert.Equal(found1, found2); + Assert.Equal(found1, found3); + } + finally + { + DeleteAll(multis); + } + } + + private static TestMulti CreateMulti(Map map, Point3D location) + { + var multi = new TestMulti(); + multi.MoveToWorld(location, map); + return multi; + } + + private static void DeleteAll(TestMulti[] multis) + { + for (var i = 0; i < multis.Length; i++) + { + multis[i]?.Delete(); + } + } + + // Test implementation of BaseMulti + private class TestMulti : BaseMulti + { + public TestMulti() : base(0x1) + { + } + } +} + diff --git a/Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs new file mode 100644 index 000000000..6e9f3c345 --- /dev/null +++ b/Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs @@ -0,0 +1,319 @@ +using System.Collections.Generic; +using Server.Items; +using Xunit; + +namespace Server.Tests.Tests.Maps; + +[Collection("Sequential Server Tests")] +public class StaticTileEnumeratorTests +{ + [Fact] + public void StaticTileEnumerator_MapNullYieldsEmpty() + { + var enumerator = new Map.StaticTileEnumerable(null, new Point2D(0, 0)).GetEnumerator(); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void StaticTileEnumerator_EmptyLocationYieldsEmpty() + { + var map = Map.Felucca; + var location = new Point2D(100, 100); + + var tiles = new List(); + foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: true, includeMultis: false)) + { + tiles.Add(tile); + } + + // Since we don't have actual map files loaded, this should be empty + Assert.Empty(tiles); + } + + [Fact] + public void StaticTileEnumerator_IncludeStaticsOnlyWorks() + { + var map = Map.Felucca; + var location = new Point2D(200, 200); + + TestMulti multi = null; + try + { + // Create a multi at the location + multi = CreateMultiWithComponents(map, new Point3D(200, 200, 0)); + + // Get tiles with statics only (no multis) + var tiles = new List(); + foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: true, includeMultis: false)) + { + tiles.Add(tile); + } + + // Should not include multi tiles + Assert.Empty(tiles); + } + finally + { + multi?.Delete(); + } + } + + [Fact] + public void StaticTileEnumerator_IncludeMultisOnlyWorks() + { + var map = Map.Felucca; + var location = new Point2D(300, 300); + + TestMulti multi = null; + try + { + // Create a multi at the location with components + multi = CreateMultiWithComponents(map, new Point3D(300, 300, 0)); + + // Get tiles with multis only (no statics) + var tiles = new List(); + foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: false, includeMultis: true)) + { + tiles.Add(tile); + } + + // Should include multi tiles + Assert.NotEmpty(tiles); + } + finally + { + multi?.Delete(); + } + } + + [Fact] + public void StaticTileEnumerator_IncludeBothStaticsAndMultisWorks() + { + var map = Map.Felucca; + var location = new Point2D(400, 400); + + TestMulti multi = null; + try + { + // Create a multi at the location + multi = CreateMultiWithComponents(map, new Point3D(400, 400, 0)); + + // Get all tiles (statics and multis) + var tiles = new List(); + foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: true, includeMultis: true)) + { + tiles.Add(tile); + } + + // Should include multi tiles (statics would be empty without map files) + Assert.NotEmpty(tiles); + } + finally + { + multi?.Delete(); + } + } + + [Fact] + public void StaticTileEnumerator_MultiTileZOffsetApplied() + { + var map = Map.Felucca; + var location = new Point2D(500, 500); + var multiZ = 10; + + TestMulti multi = null; + try + { + // Create a multi at Z=10 + multi = CreateMultiWithComponents(map, new Point3D(500, 500, multiZ)); + + // Get multi tiles + var tiles = new List(); + foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: false, includeMultis: true)) + { + tiles.Add(tile); + } + + // All tiles should have Z offset by the multi's Z position + Assert.NotEmpty(tiles); + Assert.All(tiles, tile => Assert.True(tile.Z >= multiZ)); + } + finally + { + multi?.Delete(); + } + } + + [Fact] + public void StaticTileEnumerator_TileMatrixGetStaticTilesWorks() + { + var map = Map.Felucca; + var x = 600; + var y = 600; + + // Test the TileMatrix.GetStaticTiles method + var tiles = new List(); + foreach (var tile in map.Tiles.GetStaticTiles(x, y)) + { + tiles.Add(tile); + } + + // Without map files loaded, should be empty + Assert.Empty(tiles); + } + + [Fact] + public void StaticTileEnumerator_TileMatrixGetStaticAndMultiTilesWorks() + { + var map = Map.Felucca; + var x = 700; + var y = 700; + + TestMulti multi = null; + try + { + // Create a multi at the location + multi = CreateMultiWithComponents(map, new Point3D(700, 700, 0)); + + // Test the TileMatrix.GetStaticAndMultiTiles method + var tiles = new List(); + foreach (var tile in map.Tiles.GetStaticAndMultiTiles(x, y)) + { + tiles.Add(tile); + } + + // Should include multi tiles + Assert.NotEmpty(tiles); + } + finally + { + multi?.Delete(); + } + } + + [Fact] + public void StaticTileEnumerator_TileMatrixGetMultiTilesWorks() + { + var map = Map.Felucca; + var x = 800; + var y = 800; + + TestMulti multi = null; + try + { + // Create a multi at the location + multi = CreateMultiWithComponents(map, new Point3D(800, 800, 0)); + + // Test the TileMatrix.GetMultiTiles method + var tiles = new List(); + foreach (var tile in map.Tiles.GetMultiTiles(x, y)) + { + tiles.Add(tile); + } + + // Should include multi tiles + Assert.NotEmpty(tiles); + } + finally + { + multi?.Delete(); + } + } + + [Fact] + public void StaticTileEnumerator_MultipleMultisAtSameLocation() + { + var map = Map.Felucca; + var location = new Point2D(900, 900); + + var multis = new TestMulti[2]; + try + { + // Create two multis at the same location + multis[0] = CreateMultiWithComponents(map, new Point3D(900, 900, 0)); + multis[1] = CreateMultiWithComponents(map, new Point3D(900, 900, 5)); + + // Get all multi tiles + var tiles = new List(); + foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: false, includeMultis: true)) + { + tiles.Add(tile); + } + + // Should include tiles from both multis + Assert.NotEmpty(tiles); + // We expect at least tiles from both multis + Assert.True(tiles.Count >= 2); + } + finally + { + multis[0]?.Delete(); + multis[1]?.Delete(); + } + } + + [Fact] + public void StaticTileEnumerator_EmptyReturnsCorrectly() + { + var enumerator = Map.StaticTileEnumerable.Empty.GetEnumerator(); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void StaticTileEnumerator_DeletedMultiSkipped() + { + var map = Map.Felucca; + var location = new Point2D(1000, 1000); + + var multis = new TestMulti[2]; + try + { + // Create two multis + multis[0] = CreateMultiWithComponents(map, new Point3D(1000, 1000, 0)); + multis[1] = CreateMultiWithComponents(map, new Point3D(1000, 1000, 5)); + + // Delete the first multi + multis[0].Delete(); + + // Get all multi tiles + var tiles = new List(); + foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: false, includeMultis: true)) + { + tiles.Add(tile); + } + + // Should only include tiles from the second multi + Assert.NotEmpty(tiles); + Assert.All(tiles, tile => Assert.True(tile.Z >= 5)); + } + finally + { + multis[0]?.Delete(); + multis[1]?.Delete(); + } + } + + private static TestMulti CreateMultiWithComponents(Map map, Point3D location) + { + var multi = new TestMulti(World.NewItem); + multi.MoveToWorld(location, map); + return multi; + } + + private class TestMulti : BaseMulti + { + public TestMulti(Serial serial) : base(serial) + { + } + + public override MultiComponentList Components => DefaultComponents; + + private static readonly MultiComponentList DefaultComponents = new( + [ + new MultiTileEntry(0x1, 0, 0, 0, 0x0), + new MultiTileEntry(0x2, 1, 0, 0, 0x0) + ] + ); + } +} + diff --git a/Projects/Server/Items/BaseMulti.SectorMultiLinkList.cs b/Projects/Server/Items/BaseMulti.SectorMultiLinkList.cs deleted file mode 100644 index 973e1bf4c..000000000 --- a/Projects/Server/Items/BaseMulti.SectorMultiLinkList.cs +++ /dev/null @@ -1,503 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: BaseMulti.SectorMultiLinkList.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.Items; - -// Adds support for the specific value link list on sectors for multis, separate from items -public partial class BaseMulti -{ - // Sectors, specifically for multis - public BaseMulti SectorMultiNext { get; set; } - public BaseMulti SectorMultiPrevious { get; set; } - public bool OnSectorMultiLinkList { get; set; } -} - -public struct SectorMultiValueLinkList -{ - public int Count { get; internal set; } - internal BaseMulti _first; - internal BaseMulti _last; - - public int Version { get; private set; } - - public void Remove(BaseMulti node) - { - if (node == null) - { - return; - } - - if (!node.OnSectorMultiLinkList) - { - throw new ArgumentException("Attempted to remove a node that is not on the list."); - } - - if (node.SectorMultiPrevious == null) - { - // If SectorMultiPrevious is null, then it is the first element. - if (_first != node) - { - throw new ArgumentException("Attempted to remove a node that is not on the list."); - } - - if (_first == _last) - { - _last = null; - _first = null; - } - else - { - _first = node.SectorMultiNext; - } - - if (node.SectorMultiNext != null) - { - node.SectorMultiNext.SectorMultiPrevious = null; - } - } - else - { - node.SectorMultiPrevious.SectorMultiNext = node.SectorMultiNext; - - // If SectorMultiNext is null, then it is the last element. - if (node.SectorMultiNext == null) - { - _last = node.SectorMultiPrevious; - } - else - { - node.SectorMultiNext.SectorMultiPrevious = node.SectorMultiPrevious; - } - } - - node.SectorMultiNext = null; - node.SectorMultiPrevious = null; - node.OnSectorMultiLinkList = false; - Count--; - Version++; - - if (Count < 0) - { - throw new Exception("Count is negative!"); - } - } - - // Remove all entries before this node, not including this node. - public void RemoveAllBefore(BaseMulti e) - { - if (e == null) - { - return; - } - - if (!e.OnSectorMultiLinkList) - { - throw new ArgumentException("Attempted to remove nodes before a node that is not on the list."); - } - - if (e.SectorMultiPrevious == null) - { - return; - } - - var current = e.SectorMultiPrevious; - e.SectorMultiPrevious = null; - - while (current != null) - { - var SectorMultiPrevious = current.SectorMultiPrevious; - - current.OnSectorMultiLinkList = false; - current.SectorMultiNext = null; - current.SectorMultiPrevious = null; - Count--; - - if (Count < 0) - { - throw new Exception("Count is negative!"); - } - - current = SectorMultiPrevious; - } - - _first = e; - Version++; - } - - // Remove all entries after this node, not including this node. - public void RemoveAllAfter(BaseMulti e) - { - if (e == null) - { - return; - } - - if (!e.OnSectorMultiLinkList) - { - throw new ArgumentException("Attempted to remove nodes after a node that is not on the list."); - } - - if (e.SectorMultiNext == null) - { - return; - } - - var current = e.SectorMultiNext; - e.SectorMultiNext = null; - - while (current != null) - { - var SectorMultiNext = current.SectorMultiNext; - - current.OnSectorMultiLinkList = false; - current.SectorMultiNext = null; - current.SectorMultiPrevious = null; - Count--; - - if (Count < 0) - { - throw new Exception("Count is negative!"); - } - - current = SectorMultiNext; - } - - _last = e; - Version++; - } - - public void AddLast(BaseMulti e) - { - if (e == null) - { - return; - } - - if (e.OnSectorMultiLinkList) - { - throw new ArgumentException("Attempted to add a node that is already on a list."); - } - - if (_last != null) - { - AddAfter(_last, e); - } - else - { - _first = e; - _last = e; - Count = 1; - Version++; - e.OnSectorMultiLinkList = true; - } - } - - public void AddFirst(BaseMulti e) - { - if (e == null) - { - return; - } - - if (e.OnSectorMultiLinkList) - { - throw new ArgumentException("Attempted to add a node that is already on a list."); - } - - if (_first != null) - { - AddBefore(_first, e); - } - else - { - _first = e; - _last = e; - Count = 1; - Version++; - e.OnSectorMultiLinkList = true; - } - } - - public void AddBefore(BaseMulti existing, BaseMulti node) - { - if (node == null) - { - return; - } - - ArgumentNullException.ThrowIfNull(existing); - - if (!existing.OnSectorMultiLinkList) - { - throw new ArgumentException($"Argument '{nameof(existing)}' must be a node on a list."); - } - - if (node.OnSectorMultiLinkList) - { - throw new ArgumentException("Attempted to add a node that is already on a list."); - } - - node.SectorMultiNext = existing; - node.SectorMultiPrevious = existing.SectorMultiPrevious; - - if (existing.SectorMultiPrevious != null) - { - existing.SectorMultiPrevious.SectorMultiNext = node; - } - else - { - _first = node; - } - - existing.SectorMultiPrevious = node; - node.OnSectorMultiLinkList = true; - Count++; - Version++; - } - - public void AddAfter(BaseMulti existing, BaseMulti node) - { - if (node == null) - { - return; - } - - ArgumentNullException.ThrowIfNull(existing); - - if (!existing.OnSectorMultiLinkList) - { - throw new ArgumentException($"Argument '{nameof(existing)}' must be a node on a list."); - } - - if (node.OnSectorMultiLinkList) - { - throw new ArgumentException("Attempted to add a node that is already on a list."); - } - - node.SectorMultiPrevious = existing; - node.SectorMultiNext = existing.SectorMultiNext; - - if (existing.SectorMultiNext != null) - { - existing.SectorMultiNext.SectorMultiPrevious = node; - } - else - { - _last = node; - } - - existing.SectorMultiNext = node; - node.OnSectorMultiLinkList = true; - Count++; - Version++; - } - - public void RemoveAll() - { - var current = _first; - while (current != null) - { - var SectorMultiNext = current.SectorMultiNext; - - current.OnSectorMultiLinkList = false; - current.SectorMultiNext = null; - current.SectorMultiPrevious = null; - current = SectorMultiNext; - } - - _first = null; - _last = null; - Count = 0; - Version++; - } - - public void AddLast(ref SectorMultiValueLinkList otherList, BaseMulti start, BaseMulti end) - { - // Should we check if start and end actually exist on the other list? - if (otherList.Count == 0 || otherList.Count == 1 && (start != end || otherList._first != start)) - { - throw new ArgumentException("Attempted to add nodes that are not on the specified linklist."); - } - - if (start.SectorMultiPrevious != null) - { - start.SectorMultiPrevious.SectorMultiNext = end.SectorMultiNext; - } - else - { - // Start is first - otherList._first = end.SectorMultiNext; - } - - if (end.SectorMultiNext != null) - { - end.SectorMultiNext.SectorMultiPrevious = start.SectorMultiPrevious; - } - else - { - otherList._last = start.SectorMultiPrevious; - } - - var count = 1; - var current = start; - - // Assume start and end are in the right order, or bad things happen (crash). - while (current != end) - { - count++; - current = current.SectorMultiNext; - } - - otherList.Count -= count; - - if (otherList.Count < 0) - { - throw new Exception("Count is negative!"); - } - - if (_last != null) - { - _last.SectorMultiNext = start; - start.SectorMultiPrevious = _last; - } - else - { - _first = start; - } - - _last = end; - Count += count; - Version++; - } - - public BaseMulti[] ToArray() - { - var arr = new BaseMulti[Count]; - - var index = 0; - foreach (var t in this) - { - arr[index++] = t; - } - - return arr; - } - - public ref struct SectorMultiValueListEnumerator - { - private bool _started; - private BaseMulti _current; - private ref readonly SectorMultiValueLinkList _linkList; - private int _version; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SectorMultiValueListEnumerator(in SectorMultiValueLinkList linkList) - { - _linkList = ref linkList; - _started = false; - _current = null; - _version = 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() - { - if (!_started) - { - _current = _linkList._first; - _started = true; - _version = _linkList.Version; - } - else if (_linkList.Version != _version) - { - throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); - } - else - { - _current = _current.SectorMultiNext; - } - - return _current != null; - } - - public BaseMulti Current - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _current; - } - } - - public ref struct DescendingSectorMultiValueListEnumerator - { - private bool _started; - private BaseMulti _current; - private ref readonly SectorMultiValueLinkList _linkList; - private int _version; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public DescendingSectorMultiValueListEnumerator(in SectorMultiValueLinkList linkList) - { - _linkList = ref linkList; - _started = false; - _current = null; - _version = 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() - { - if (!_started) - { - _current = _linkList._last; - _started = true; - _version = _linkList.Version; - } - else if (_linkList.Version != _version) - { - throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); - } - else - { - _current = _current.SectorMultiPrevious; - } - - return _current != null; - } - - public BaseMulti Current - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _current; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public DescendingSectorMultiValueListEnumerator GetEnumerator() => this; - } -} - -public static class SectorMultiValueLinkListExt -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static SectorMultiValueLinkList.SectorMultiValueListEnumerator GetEnumerator(this in SectorMultiValueLinkList linkList) - => new(in linkList); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static SectorMultiValueLinkList.DescendingSectorMultiValueListEnumerator ByDescending(this in SectorMultiValueLinkList linkList) - => new(in linkList); -} diff --git a/Projects/Server/Maps/Map.ClientEnumerator.cs b/Projects/Server/Maps/Map.ClientEnumerator.cs index ae53010a1..d4dea8e26 100644 --- a/Projects/Server/Maps/Map.ClientEnumerator.cs +++ b/Projects/Server/Maps/Map.ClientEnumerator.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Map.ClientEnumerator.cs * * * @@ -76,6 +76,7 @@ public partial class Map public ref struct ClientAtEnumerator { + private readonly Map _map; private bool _started; private Point2D _location; private ref readonly ValueLinkList _linkList; @@ -85,9 +86,15 @@ public partial class Map [MethodImpl(MethodImplOptions.AggressiveInlining)] public ClientAtEnumerator(Map map, Point2D loc) { + _map = map; _started = false; _location = loc; - _linkList = ref map.GetSector(loc.m_X, loc.m_Y).Clients; + + if (map != null) + { + _linkList = ref map.GetSector(loc.m_X, loc.m_Y).Clients; + } + _version = 0; _current = null; } @@ -95,6 +102,11 @@ public partial class Map [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { + if (_map == null) + { + return false; + } + ref var loc = ref _location; NetState current; Mobile m; @@ -105,7 +117,7 @@ public partial class Map _started = true; _version = _linkList.Version; - m = current.Mobile; + m = current?.Mobile; if (m?.Deleted == false && m.X == loc.m_X && m.Y == loc.m_Y) { _current = current; @@ -125,7 +137,7 @@ public partial class Map { current = current.Next; - m = current.Mobile; + m = current?.Mobile; if (m?.Deleted == false && m.X == loc.m_X && m.Y == loc.m_Y) { _current = current; @@ -163,10 +175,10 @@ public partial class Map } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public MobileEnumerator GetEnumerator() => new(_map, _bounds, _makeBoundsInclusive); + public ClientBoundsEnumerator GetEnumerator() => new(_map, _bounds, _makeBoundsInclusive); } - public ref struct MobileEnumerator + public ref struct ClientBoundsEnumerator { private readonly Map _map; private readonly int _sectorStartX; @@ -182,7 +194,7 @@ public partial class Map private NetState _current; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public MobileEnumerator(Map map, Rectangle2D bounds, bool makeBoundsInclusive) + public ClientBoundsEnumerator(Map map, Rectangle2D bounds, bool makeBoundsInclusive) { _map = map; _bounds = bounds; diff --git a/Projects/Server/Maps/Map.MultiEnumerator.cs b/Projects/Server/Maps/Map.MultiEnumerator.cs index bc1f7b018..807e351b1 100644 --- a/Projects/Server/Maps/Map.MultiEnumerator.cs +++ b/Projects/Server/Maps/Map.MultiEnumerator.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Map.MultiEnumerator.cs * * * @@ -17,15 +17,13 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Server.Collections; using Server.Items; namespace Server; public partial class Map { - private static SectorMultiValueLinkList _emptyMultiLinkList = new(); - public static ref readonly SectorMultiValueLinkList EmptyMultiLinkList => ref _emptyMultiLinkList; - [MethodImpl(MethodImplOptions.AggressiveInlining)] public MultiSectorEnumerable GetMultisInSector(Point3D p) => GetMultisInSector(p); @@ -83,6 +81,8 @@ public partial class Map public MultiBoundsEnumerable GetMultisInBounds(Rectangle2D bounds, bool makeBoundsInclusive = false) where T : BaseMulti => new(this, bounds, makeBoundsInclusive); + private static readonly HashSet _sharedDupes = []; + public ref struct MultiSectorEnumerable(Map map, Point2D loc) where T : BaseMulti { public static MultiSectorEnumerable Empty @@ -98,27 +98,42 @@ public partial class Map public ref struct MultiSectorEnumerator where T : BaseMulti { private readonly Span _list; + private readonly int _version; + private readonly Sector _sector; private int _index; private T _current; [MethodImpl(MethodImplOptions.AggressiveInlining)] public MultiSectorEnumerator(Map map, Point2D loc) { - _list = map == null - ? Span.Empty - : CollectionsMarshal.AsSpan(map.GetSector(loc.m_X, loc.m_Y).Multis); + if (map == null) + { + _list = Span.Empty; + _sector = null; + _version = 0; + } + else + { + _sector = map.GetSector(loc.m_X, loc.m_Y); + _list = CollectionsMarshal.AsSpan(_sector.Multis); + _version = _sector.MultisVersion; + } - _index = 0; + _index = -1; _current = null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - while ((uint)_index < (uint)_list.Length) + if (_sector != null && _version != _sector.MultisVersion) { - var current = _list[_index++]; - if (current is T { Deleted: false } o) + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); + } + + while (++_index < _list.Length) + { + if (_list[_index] is T { Deleted: false } o) { _current = o; return true; @@ -160,24 +175,27 @@ public partial class Map public ref struct MultiBoundsEnumerator where T : BaseMulti { - private readonly Map _map; - private readonly int _sectorStartX; - private readonly int _sectorEndX; - private readonly int _sectorEndY; + private Map _map; + private int _sectorStartX; + private int _sectorEndX; + private int _sectorEndY; private Rectangle2D _bounds; private int _currentSectorX; private int _currentSectorY; - private Span _list; + private Span _currentList; + private int _currentIndex; + private int _currentVersion; + private Sector _currentSector; private T _current; - private int _index; - private HashSet _dupes; [MethodImpl(MethodImplOptions.AggressiveInlining)] public MultiBoundsEnumerator(Map map, Rectangle2D bounds, bool makeBoundsInclusive) { + _sharedDupes.Clear(); + _map = map; _bounds = bounds; @@ -196,62 +214,78 @@ public partial class Map // We start the X sector one short because it gets incremented immediately in MoveNext() _currentSectorX = _sectorStartX - 1; _currentSectorY = _sectorStartY; - _index = 0; } + + _currentList = default; + _currentIndex = -1; + _currentVersion = 0; + _currentSector = null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool GetMulti() + public bool MoveNext() { - ref Rectangle2D bounds = ref _bounds; + var map = _map; - while ((uint)_index < (uint)_list.Length) + if (map == null) { - var current = _list[_index++]; - _dupes ??= new HashSet(); - - if (current is T { Deleted: false } o && bounds.Contains(o.Location) && !_dupes.Contains(o.Serial)) - { - _dupes.Add(o.Serial); - _current = o; - return true; - } + return false; } - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool GetSector() - { + ref Rectangle2D bounds = ref _bounds; var currentSectorX = _currentSectorX; var currentSectorY = _currentSectorY; var sectorEndX = _sectorEndX; var sectorEndY = _sectorEndY; - // Move to next sector - if (currentSectorX < sectorEndX) + while (true) { - _currentSectorX = ++currentSectorX; - } - else if (currentSectorY < sectorEndY) - { - _currentSectorX = currentSectorX = _sectorStartX; - _currentSectorY = ++currentSectorY; - } - else - { - // Ran out of sectors - return false; - } + // Try to advance in the current list + if (_currentList.Length > 0) + { + if (_currentVersion != _currentSector.MultisVersion) + { + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); + } - _list = CollectionsMarshal.AsSpan(_map.GetRealSector(currentSectorX, currentSectorY).Multis); - return GetMulti(); + while (++_currentIndex < _currentList.Length) + { + var item = _currentList[_currentIndex]; + if (item is T { Deleted: false } o && bounds.Contains(o.Location)) + { + // Multis can span multiple sectors, so we need to deduplicate + if (_sharedDupes.Add(o.Serial)) + { + _current = o; + return true; + } + } + } + } + + // Move to next sector + if (currentSectorX < sectorEndX) + { + _currentSectorX = ++currentSectorX; + } + else if (currentSectorY < sectorEndY) + { + _currentSectorX = currentSectorX = _sectorStartX; + _currentSectorY = ++currentSectorY; + } + else + { + // Ran out of sectors + return false; + } + + _currentSector = map.GetRealSector(currentSectorX, currentSectorY); + _currentList = CollectionsMarshal.AsSpan(_currentSector.Multis); + _currentVersion = _currentSector.MultisVersion; + _currentIndex = -1; + } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() => _map != null && (GetMulti() || GetSector()); - public T Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/Server/Maps/Map.cs b/Projects/Server/Maps/Map.cs index 26ab156b8..69361f855 100644 --- a/Projects/Server/Maps/Map.cs +++ b/Projects/Server/Maps/Map.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Map.cs * * * @@ -1355,11 +1355,13 @@ public sealed partial class Map : IComparable, ISpanFormattable, ISpanParsa { // TODO: Can we avoid this? private static readonly List m_DefaultRectList = new(); + private static readonly List m_DefaultMultiList = new(); private bool m_Active; private ValueLinkList _clients; private ValueLinkList _items; private ValueLinkList _mobiles; - private List _multis = new(); + private List _multis; + private int _multisVersion; private List _regions; public Sector(int x, int y, Map owner) @@ -1372,9 +1374,11 @@ public sealed partial class Map : IComparable, ISpanFormattable, ISpanParsa public List Regions => _regions ?? m_DefaultRectList; - internal List Multis => _multis; + internal List Multis => _multis ?? m_DefaultMultiList; - internal ref ValueLinkList Mobiles => ref _mobiles; + internal int MultisVersion => _multisVersion; + + internal ref readonly ValueLinkList Mobiles => ref _mobiles; internal ref readonly ValueLinkList Items => ref _items; @@ -1503,12 +1507,17 @@ public sealed partial class Map : IComparable, ISpanFormattable, ISpanParsa public void OnMultiEnter(BaseMulti multi) { + _multis ??= new List(); _multis.Add(multi); + _multisVersion++; } public void OnMultiLeave(BaseMulti multi) { - _multis.Remove(multi); + if (_multis?.Remove(multi) == true) + { + _multisVersion++; + } } public void Activate()