diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 08b49e256..5e8c8ad9b 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"modernuoschemagenerator": {
- "version": "2.12.20",
+ "version": "2.13.0",
"commands": [
"ModernUOSchemaGenerator"
]
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index c2f1c3b2f..15d42fceb 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- - name: Install .NET 9
+ - name: Install .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
@@ -54,20 +54,14 @@ jobs:
- container: ubuntu:jammy
name: Ubuntu 22
packageManager: apt
- - container: ubuntu:focal
- name: Ubuntu 20
+ - container: debian:trixie
+ name: Debian 13
packageManager: apt
- container: debian:bookworm
name: Debian 12
packageManager: apt
- - container: debian:bullseye
- name: Debian 11
- packageManager: apt
- - container: fedora:39
- name: Fedora 39
- packageManager: dnf
- - container: fedora:40
- name: Fedora 40
+ - container: fedora:42
+ name: Fedora 42
packageManager: dnf
- container: quay.io/centos/centos:stream9
name: CentOS 9 Stream
@@ -86,7 +80,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- - name: Install .NET 9
+ - name: Install .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
index 0e8334e59..aa02e4489 100644
--- a/.github/workflows/create-release.yml
+++ b/.github/workflows/create-release.yml
@@ -14,7 +14,7 @@ jobs:
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- - name: Install .NET 9
+ - name: Install .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
diff --git a/Directory.Build.props b/Directory.Build.props
index a9eae192f..57f39a926 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,9 +3,9 @@
Kamron Batman
ModernUO
- 2019-2024
- net9.0
- 13
+ 2019-2025
+ net10.0
+ 14
true
true
NU1603
@@ -62,11 +62,11 @@
latest
-
+
-
+
- 3.7.115
+ 3.9.50
all
diff --git a/Projects/Server.Tests/Server.Tests.csproj b/Projects/Server.Tests/Server.Tests.csproj
index 8da175d26..0f913dd48 100644
--- a/Projects/Server.Tests/Server.Tests.csproj
+++ b/Projects/Server.Tests/Server.Tests.csproj
@@ -5,9 +5,9 @@
Server.Tests
-
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs b/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs
new file mode 100644
index 000000000..9a68576c8
--- /dev/null
+++ b/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs
@@ -0,0 +1,597 @@
+using System;
+using System.Buffers;
+using System.IO;
+using Xunit;
+
+namespace Server.Tests;
+
+public class SpanReaderTests
+{
+ [Fact]
+ public void TestReadByte()
+ {
+ ReadOnlySpan buffer = [0x12, 0x34];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x12, reader.ReadByte());
+ Assert.Equal(0x34, reader.ReadByte());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadByteAtEnd()
+ {
+ Assert.Throws(
+ () =>
+ {
+ ReadOnlySpan buffer = [0x12];
+ var reader = new SpanReader(buffer);
+ reader.ReadByte();
+ reader.ReadByte();
+ }
+ );
+ }
+
+ [Fact]
+ public void TestReadBoolean()
+ {
+ ReadOnlySpan buffer = [0, 1, 2, 255];
+ var reader = new SpanReader(buffer);
+
+ Assert.False(reader.ReadBoolean());
+ Assert.True(reader.ReadBoolean());
+ Assert.True(reader.ReadBoolean());
+ Assert.True(reader.ReadBoolean());
+ Assert.Equal(4, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadSByte()
+ {
+ ReadOnlySpan buffer = [0xFF, 0x7F];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(-1, reader.ReadSByte());
+ Assert.Equal(127, reader.ReadSByte());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt16BigEndian()
+ {
+ ReadOnlySpan buffer = [0x12, 0x34];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x1234, reader.ReadInt16());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt16LittleEndian()
+ {
+ ReadOnlySpan buffer = [0x34, 0x12];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x1234, reader.ReadInt16LE());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt16AtEnd()
+ {
+ Assert.Throws(
+ () =>
+ {
+ ReadOnlySpan buffer = [0x12];
+ var reader = new SpanReader(buffer);
+ reader.ReadInt16();
+ }
+ );
+ }
+
+ [Fact]
+ public void TestReadUInt16BigEndian()
+ {
+ ReadOnlySpan buffer = [0x12, 0x34];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal((ushort)0x1234, reader.ReadUInt16());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadUInt16LittleEndian()
+ {
+ ReadOnlySpan buffer = [0x34, 0x12];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal((ushort)0x1234, reader.ReadUInt16LE());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt32BigEndian()
+ {
+ ReadOnlySpan buffer = [0x12, 0x34, 0x56, 0x78];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x12345678, reader.ReadInt32());
+ Assert.Equal(4, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt32AtEnd()
+ {
+ Assert.Throws(
+ () =>
+ {
+ ReadOnlySpan buffer = [0x12, 0x34, 0x56];
+ var reader = new SpanReader(buffer);
+ reader.ReadInt32();
+ }
+ );
+ }
+
+ [Fact]
+ public void TestReadUInt32BigEndian()
+ {
+ ReadOnlySpan buffer = [0x12, 0x34, 0x56, 0x78];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x12345678u, reader.ReadUInt32());
+ Assert.Equal(4, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadUInt32LittleEndian()
+ {
+ ReadOnlySpan buffer = [0x78, 0x56, 0x34, 0x12];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x12345678u, reader.ReadUInt32LE());
+ Assert.Equal(4, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt64BigEndian()
+ {
+ ReadOnlySpan buffer = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x123456789ABCDEF0L, reader.ReadInt64());
+ Assert.Equal(8, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt64AtEnd()
+ {
+ Assert.Throws(
+ () =>
+ {
+ ReadOnlySpan buffer = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE];
+ var reader = new SpanReader(buffer);
+ reader.ReadInt64();
+ }
+ );
+ }
+
+ [Fact]
+ public void TestReadUInt64BigEndian()
+ {
+ ReadOnlySpan buffer = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x123456789ABCDEF0UL, reader.ReadUInt64());
+ Assert.Equal(8, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadAsciiString()
+ {
+ ReadOnlySpan buffer = "Hello"u8;
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadAscii();
+
+ Assert.Equal("Hello", result);
+ Assert.Equal(5, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadAsciiStringWithNull()
+ {
+ ReadOnlySpan buffer = "Hel\0o"u8;
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadAscii();
+
+ Assert.Equal("Hel", result);
+ Assert.Equal(4, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadAsciiStringFixedLength()
+ {
+ ReadOnlySpan buffer = "Hello\0\0\0\0\0"u8;
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadAscii(10);
+
+ Assert.Equal("Hello", result);
+ Assert.Equal(10, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadAsciiStringFixedLengthTooLarge()
+ {
+ Assert.Throws(
+ () =>
+ {
+ ReadOnlySpan buffer = "Hel"u8;
+ var reader = new SpanReader(buffer);
+ reader.ReadAscii(10);
+ }
+ );
+ }
+
+ [Fact]
+ public void TestReadAsciiSafe()
+ {
+ ReadOnlySpan buffer = [(byte)'H', (byte)'e', 0xFF, (byte)'l', (byte)'o'];
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadAsciiSafe();
+
+ Assert.Equal("He?lo", result);
+ Assert.Equal(5, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadUTF8String()
+ {
+ ReadOnlySpan buffer = "Hello"u8;
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadUTF8();
+
+ Assert.Equal("Hello", result);
+ Assert.Equal(5, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadUTF8StringWithNull()
+ {
+ ReadOnlySpan buffer = "Hi\0Bye"u8;
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadUTF8();
+
+ Assert.Equal("Hi", result);
+ Assert.Equal(3, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadLittleUniString()
+ {
+ ReadOnlySpan buffer = "H\0i\0"u8;
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadLittleUni();
+
+ Assert.Equal("Hi", result);
+ Assert.Equal(4, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadLittleUniStringWithNull()
+ {
+ ReadOnlySpan buffer = "H\0i\0\0\0X\0"u8;
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadLittleUni();
+
+ Assert.Equal("Hi", result);
+ Assert.Equal(5, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadLittleUniStringFixedLength()
+ {
+ ReadOnlySpan buffer = "H\0i\0\0\0\0\0"u8;
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadLittleUni(4);
+
+ Assert.Equal("Hi", result);
+ Assert.Equal(8, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadLittleUniSafe()
+ {
+ ReadOnlySpan buffer = [(byte)'H', 0, 0xFF, 0xD8, (byte)'i', 0];
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadLittleUniSafe();
+
+ Assert.Equal("H\uFFFDi", result);
+ Assert.Equal(6, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadBigUniString()
+ {
+ ReadOnlySpan buffer = "\0H\0i"u8;
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadBigUni();
+
+ Assert.Equal("Hi", result);
+ Assert.Equal(4, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadBigUniStringWithNull()
+ {
+ ReadOnlySpan buffer = "\0H\0i\0\0\0X"u8;
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadBigUni();
+
+ Assert.Equal("Hi", result);
+ Assert.Equal(5, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadBigUniStringFixedLength()
+ {
+ ReadOnlySpan buffer = "\0H\0i\0\0\0\0"u8;
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadBigUni(4);
+
+ Assert.Equal("Hi", result);
+ Assert.Equal(8, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadBigUniSafe()
+ {
+ ReadOnlySpan buffer = [0, (byte)'H', 0xD8, 0xFF, 0, (byte)'i'];
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadBigUniSafe();
+
+ Assert.Equal("H\uFFFDi", result);
+ Assert.Equal(6, reader.Position);
+ }
+
+ [Fact]
+ public void TestSeekBegin()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05];
+ var reader = new SpanReader(buffer);
+
+ reader.ReadByte();
+ reader.ReadByte();
+
+ var pos = reader.Seek(0, SeekOrigin.Begin);
+
+ Assert.Equal(0, pos);
+ Assert.Equal(0, reader.Position);
+ Assert.Equal(0x01, reader.ReadByte());
+ }
+
+ [Fact]
+ public void TestSeekCurrent()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05];
+ var reader = new SpanReader(buffer);
+
+ reader.ReadByte();
+ var pos = reader.Seek(2, SeekOrigin.Current);
+
+ Assert.Equal(3, pos);
+ Assert.Equal(3, reader.Position);
+ Assert.Equal(0x04, reader.ReadByte());
+ }
+
+ [Fact]
+ public void TestSeekEnd()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05];
+ var reader = new SpanReader(buffer);
+
+ var pos = reader.Seek(-2, SeekOrigin.End);
+
+ Assert.Equal(3, pos);
+ Assert.Equal(3, reader.Position);
+ Assert.Equal(0x04, reader.ReadByte());
+ }
+
+ [Fact]
+ public void TestSeekNegativeThrows()
+ {
+ Assert.Throws(
+ () =>
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03];
+ var reader = new SpanReader(buffer);
+ reader.Seek(-1, SeekOrigin.Begin);
+ }
+ );
+ }
+
+ [Fact]
+ public void TestSeekBeyondEndThrows()
+ {
+ Assert.Throws(
+ () =>
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03];
+ var reader = new SpanReader(buffer);
+ reader.Seek(10, SeekOrigin.Begin);
+ }
+ );
+ }
+
+ [Fact]
+ public void TestRead()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05];
+ var reader = new SpanReader(buffer);
+
+ Span dest = stackalloc byte[3];
+ var bytesRead = reader.Read(dest);
+
+ Assert.Equal(3, bytesRead);
+ Assert.Equal(3, reader.Position);
+ AssertThat.Equal(dest, [0x01, 0x02, 0x03]);
+ }
+
+ [Fact]
+ public void TestReadPartial()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03];
+ var reader = new SpanReader(buffer);
+
+ Span dest = stackalloc byte[5];
+ var bytesRead = reader.Read(dest);
+
+ Assert.Equal(3, bytesRead);
+ Assert.Equal(3, reader.Position);
+ AssertThat.Equal(dest[..3], [0x01, 0x02, 0x03]);
+ }
+
+ [Fact]
+ public void TestReadEmpty()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03];
+ var reader = new SpanReader(buffer);
+
+ Span dest = [];
+ var bytesRead = reader.Read(dest);
+
+ Assert.Equal(0, bytesRead);
+ Assert.Equal(0, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadAtEnd()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03];
+ var reader = new SpanReader(buffer);
+
+ reader.Seek(3, SeekOrigin.Begin);
+
+ Span dest = stackalloc byte[5];
+ var bytesRead = reader.Read(dest);
+
+ Assert.Equal(0, bytesRead);
+ Assert.Equal(3, reader.Position);
+ }
+
+ [Fact]
+ public void TestLength()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(5, reader.Length);
+ }
+
+ [Fact]
+ public void TestPosition()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0, reader.Position);
+
+ reader.ReadByte();
+ Assert.Equal(1, reader.Position);
+
+ reader.ReadUInt16();
+ Assert.Equal(3, reader.Position);
+ }
+
+ [Fact]
+ public void TestRemaining()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(5, reader.Remaining);
+
+ reader.ReadByte();
+ Assert.Equal(4, reader.Remaining);
+
+ reader.ReadUInt16();
+ Assert.Equal(2, reader.Remaining);
+
+ reader.ReadUInt16();
+ Assert.Equal(0, reader.Remaining);
+ }
+
+ [Fact]
+ public void TestBuffer()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03];
+ var reader = new SpanReader(buffer);
+
+ var bufferProperty = reader.Buffer;
+
+ Assert.Equal(3, bufferProperty.Length);
+ AssertThat.Equal(bufferProperty, buffer);
+ }
+
+ [Fact]
+ public void TestReadStringEmptyFixedLength()
+ {
+ ReadOnlySpan buffer = [0x01, 0x02, 0x03];
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadAscii(0);
+
+ Assert.Equal("", result);
+ Assert.Equal(0, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadMultipleStringsWithNullTerminators()
+ {
+ ReadOnlySpan buffer = "AB\0CD\0EF"u8;
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal("AB", reader.ReadAscii());
+ Assert.Equal("CD", reader.ReadAscii());
+ Assert.Equal("EF", reader.ReadAscii());
+ Assert.Equal(8, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadLittleUniOddByteCount()
+ {
+ // If buffer has odd number of bytes, the last byte should be ignored
+ ReadOnlySpan buffer = [(byte)'H', 0, (byte)'i', 0, 0xFF];
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadLittleUni();
+
+ Assert.Equal("Hi", result);
+ Assert.Equal(4, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadBigUniOddByteCount()
+ {
+ // If buffer has odd number of bytes, the last byte should be ignored
+ ReadOnlySpan buffer = [0, (byte)'H', 0, (byte)'i', 0xFF];
+ var reader = new SpanReader(buffer);
+
+ var result = reader.ReadBigUni();
+
+ Assert.Equal("Hi", result);
+ Assert.Equal(4, reader.Position);
+ }
+}
diff --git a/Projects/Server.Tests/Tests/Buffers/SpanWriterTests.cs b/Projects/Server.Tests/Tests/Buffers/SpanWriterTests.cs
index 0aeffe574..a0e7ed18c 100644
--- a/Projects/Server.Tests/Tests/Buffers/SpanWriterTests.cs
+++ b/Projects/Server.Tests/Tests/Buffers/SpanWriterTests.cs
@@ -1,63 +1,591 @@
using System;
using System.Buffers;
+using System.IO;
using Xunit;
-namespace Server.Tests
+namespace Server.Tests;
+
+public class SpanWriterTests
{
- public class SpanWriterTests
+ [Fact]
+ public unsafe void TestSpanWriterResizes()
{
- [Fact]
- public unsafe void TestSpanWriterResizes()
- {
- Span smallStack = stackalloc byte[8];
- using var writer = new SpanWriter(smallStack, true);
- writer.Write(0x1024L);
- writer.Write(0x1024L);
+ Span smallStack = stackalloc byte[8];
+ var writer = new SpanWriter(smallStack, true);
+ writer.Write(0x1024L);
+ writer.Write(0x1024L);
- var span = writer.RawBuffer;
- fixed (byte* spanPtr = span)
+ var span = writer.RawBuffer;
+ fixed (byte* spanPtr = span)
+ {
+ fixed (byte* stackPtr = smallStack)
{
- fixed (byte* stackPtr = smallStack)
- {
- Assert.True(spanPtr != stackPtr);
- }
+ Assert.True(spanPtr != stackPtr);
}
-
- Assert.True(span.Length > smallStack.Length);
}
- [Fact]
- public unsafe void TestSpanWriterOnlyStackAlloc()
- {
- Span smallStack = stackalloc byte[8];
- using var writer = new SpanWriter(smallStack, true);
- writer.Write(0x1024L);
+ Assert.True(span.Length > smallStack.Length);
+ writer.Dispose();
+ }
- var span = writer.RawBuffer;
- fixed (byte* spanPtr = span)
+ [Fact]
+ public unsafe void TestSpanWriterOnlyStackAlloc()
+ {
+ Span smallStack = stackalloc byte[8];
+ var writer = new SpanWriter(smallStack, true);
+ writer.Write(0x1024L);
+
+ var span = writer.RawBuffer;
+ fixed (byte* spanPtr = span)
+ {
+ fixed (byte* stackPtr = smallStack)
{
- fixed (byte* stackPtr = smallStack)
- {
- Assert.True(spanPtr == stackPtr);
- }
+ Assert.True(spanPtr == stackPtr);
}
-
- Assert.True(span.Length == smallStack.Length);
- AssertThat.Equal(smallStack, stackalloc byte[] { 0, 0, 0, 0, 0, 0, 0x10, 0x24 });
}
- [Fact]
- public void TestSpanWriterNoResizeThrows()
- {
- Assert.Throws(
- () =>
- {
- Span smallStack = stackalloc byte[8];
- using var writer = new SpanWriter(smallStack);
- writer.Write(0x1024L);
- writer.Write(0x1024L);
- }
- );
- }
+ Assert.Equal(8, span.Length);
+ AssertThat.Equal(smallStack, [0, 0, 0, 0, 0, 0, 0x10, 0x24]);
+ }
+
+ [Fact]
+ public void TestSpanWriterNoResizeThrows()
+ {
+ Assert.Throws(
+ () =>
+ {
+ Span smallStack = stackalloc byte[8];
+ var writer = new SpanWriter(smallStack);
+ writer.Write(0x1024L);
+ writer.Write(0x1024L);
+ }
+ );
+ }
+
+ [Fact]
+ public void TestWriteBool()
+ {
+ Span buffer = stackalloc byte[2];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write(true);
+ writer.Write(false);
+
+ Assert.Equal(2, writer.Position);
+ AssertThat.Equal(writer.Span, [1, 0]);
+ }
+
+ [Fact]
+ public void TestWriteByte()
+ {
+ Span buffer = stackalloc byte[2];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write((byte)0x12);
+ writer.Write((byte)0x34);
+
+ Assert.Equal(2, writer.Position);
+ AssertThat.Equal(writer.Span, [0x12, 0x34]);
+ }
+
+ [Fact]
+ public void TestWriteSByte()
+ {
+ Span buffer = stackalloc byte[2];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write((sbyte)-1);
+ writer.Write((sbyte)127);
+
+ Assert.Equal(2, writer.Position);
+ AssertThat.Equal(writer.Span, [0xFF, 0x7F]);
+ }
+
+ [Fact]
+ public void TestWriteInt16BigEndian()
+ {
+ Span buffer = stackalloc byte[2];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write((short)0x1234);
+
+ Assert.Equal(2, writer.Position);
+ AssertThat.Equal(writer.Span, [0x12, 0x34]);
+ }
+
+ [Fact]
+ public void TestWriteInt16LittleEndian()
+ {
+ Span buffer = stackalloc byte[2];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteLE((short)0x1234);
+
+ Assert.Equal(2, writer.Position);
+ AssertThat.Equal(writer.Span, [0x34, 0x12]);
+ }
+
+ [Fact]
+ public void TestWriteUInt16BigEndian()
+ {
+ Span buffer = stackalloc byte[2];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write((ushort)0x1234);
+
+ Assert.Equal(2, writer.Position);
+ AssertThat.Equal(writer.Span, [0x12, 0x34]);
+ }
+
+ [Fact]
+ public void TestWriteUInt16LittleEndian()
+ {
+ Span buffer = stackalloc byte[2];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteLE((ushort)0x1234);
+
+ Assert.Equal(2, writer.Position);
+ AssertThat.Equal(writer.Span, [0x34, 0x12]);
+ }
+
+ [Fact]
+ public void TestWriteInt32BigEndian()
+ {
+ Span buffer = stackalloc byte[4];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write(0x12345678);
+
+ Assert.Equal(4, writer.Position);
+ AssertThat.Equal(writer.Span, [0x12, 0x34, 0x56, 0x78]);
+ }
+
+ [Fact]
+ public void TestWriteInt32LittleEndian()
+ {
+ Span buffer = stackalloc byte[4];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteLE(0x12345678);
+
+ Assert.Equal(4, writer.Position);
+ AssertThat.Equal(writer.Span, [0x78, 0x56, 0x34, 0x12]);
+ }
+
+ [Fact]
+ public void TestWriteUInt32BigEndian()
+ {
+ Span buffer = stackalloc byte[4];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write(0x12345678u);
+
+ Assert.Equal(4, writer.Position);
+ AssertThat.Equal(writer.Span, [0x12, 0x34, 0x56, 0x78]);
+ }
+
+ [Fact]
+ public void TestWriteUInt32LittleEndian()
+ {
+ Span buffer = stackalloc byte[4];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteLE(0x12345678u);
+
+ Assert.Equal(4, writer.Position);
+ AssertThat.Equal(writer.Span, [0x78, 0x56, 0x34, 0x12]);
+ }
+
+ [Fact]
+ public void TestWriteInt64BigEndian()
+ {
+ Span buffer = stackalloc byte[8];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write(0x123456789ABCDEF0L);
+
+ Assert.Equal(8, writer.Position);
+ AssertThat.Equal(writer.Span, [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);
+ }
+
+ [Fact]
+ public void TestWriteUInt64BigEndian()
+ {
+ Span buffer = stackalloc byte[8];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write(0x123456789ABCDEF0UL);
+
+ Assert.Equal(8, writer.Position);
+ AssertThat.Equal(writer.Span, [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);
+ }
+
+ [Fact]
+ public void TestWriteSpan()
+ {
+ Span buffer = stackalloc byte[6];
+ var writer = new SpanWriter(buffer);
+
+ ReadOnlySpan data = [0x01, 0x02, 0x03];
+ writer.Write(data);
+ writer.Write(data);
+
+ Assert.Equal(6, writer.Position);
+ AssertThat.Equal(writer.Span, [0x01, 0x02, 0x03, 0x01, 0x02, 0x03]);
+ }
+
+ [Fact]
+ public void TestWriteAsciiChar()
+ {
+ Span buffer = stackalloc byte[2];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteAscii('A');
+ writer.WriteAscii('B');
+
+ Assert.Equal(2, writer.Position);
+ AssertThat.Equal(writer.Span, "AB"u8);
+ }
+
+ [Fact]
+ public void TestWriteAsciiString()
+ {
+ Span buffer = stackalloc byte[5];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteAscii("Hello");
+
+ Assert.Equal(5, writer.Position);
+ AssertThat.Equal(writer.Span, "Hello"u8);
+ }
+
+ [Fact]
+ public void TestWriteAsciiStringNull()
+ {
+ Span buffer = stackalloc byte[6];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteAsciiNull("Hello");
+
+ Assert.Equal(6, writer.Position);
+ AssertThat.Equal(writer.Span, "Hello\0"u8);
+ }
+
+ [Fact]
+ public void TestWriteAsciiStringFixedLength()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteAscii("Hello", 10);
+
+ Assert.Equal(10, writer.Position);
+ AssertThat.Equal(writer.Span, "Hello\0\0\0\0\0"u8);
+ }
+
+ [Fact]
+ public void TestWriteUTF8String()
+ {
+ Span buffer = stackalloc byte[5];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteUTF8("Hello");
+
+ Assert.Equal(5, writer.Position);
+ AssertThat.Equal(writer.Span, "Hello"u8);
+ }
+
+ [Fact]
+ public void TestWriteUTF8StringNull()
+ {
+ Span buffer = stackalloc byte[6];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteUTF8Null("Hello");
+
+ Assert.Equal(6, writer.Position);
+ AssertThat.Equal(writer.Span, "Hello\0"u8);
+ }
+
+ [Fact]
+ public void TestWriteLittleUniString()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteLittleUni("Hello");
+
+ Assert.Equal(10, writer.Position);
+ AssertThat.Equal(writer.Span, "H\0e\0l\0l\0o\0"u8);
+ }
+
+ [Fact]
+ public void TestWriteLittleUniStringNull()
+ {
+ Span buffer = stackalloc byte[12];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteLittleUniNull("Hello");
+
+ Assert.Equal(12, writer.Position);
+ AssertThat.Equal(writer.Span, "H\0e\0l\0l\0o\0\0\0"u8);
+ }
+
+ [Fact]
+ public void TestWriteBigUniString()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteBigUni("Hello");
+
+ Assert.Equal(10, writer.Position);
+ AssertThat.Equal(writer.Span, "\0H\0e\0l\0l\0o"u8);
+ }
+
+ [Fact]
+ public void TestWriteBigUniStringNull()
+ {
+ Span buffer = stackalloc byte[12];
+ var writer = new SpanWriter(buffer);
+
+ writer.WriteBigUniNull("Hello");
+
+ Assert.Equal(12, writer.Position);
+ AssertThat.Equal(writer.Span, "\0H\0e\0l\0l\0o\0\0"u8);
+ }
+
+ [Fact]
+ public void TestClear()
+ {
+ Span buffer = stackalloc byte[10];
+ buffer.Fill(0xFF);
+ var writer = new SpanWriter(buffer);
+
+ writer.Write((byte)0x01);
+ writer.Clear(5);
+ writer.Write((byte)0x02);
+
+ Assert.Equal(7, writer.Position);
+ AssertThat.Equal(writer.Span, [0x01, 0, 0, 0, 0, 0, 0x02]);
+ }
+
+ [Fact]
+ public void TestSeekBegin()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write((byte)0x01);
+ writer.Write((byte)0x02);
+ writer.Write((byte)0x03);
+
+ var pos = writer.Seek(1, SeekOrigin.Begin);
+
+ Assert.Equal(1, pos);
+ Assert.Equal(1, writer.Position);
+ Assert.Equal(3, writer.BytesWritten);
+ }
+
+ [Fact]
+ public void TestSeekCurrent()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write((byte)0x01);
+ writer.Write((byte)0x02);
+
+ var pos = writer.Seek(2, SeekOrigin.Current);
+
+ Assert.Equal(4, pos);
+ Assert.Equal(4, writer.Position);
+ }
+
+ [Fact]
+ public void TestSeekEnd()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write((byte)0x01);
+ writer.Write((byte)0x02);
+ writer.Write((byte)0x03);
+
+ var pos = writer.Seek(-1, SeekOrigin.End);
+
+ Assert.Equal(2, pos);
+ Assert.Equal(2, writer.Position);
+ }
+
+ [Fact]
+ public void TestSeekNegativeThrows()
+ {
+ Assert.Throws(
+ () =>
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+ writer.Seek(-1, SeekOrigin.Begin);
+ }
+ );
+ }
+
+ [Fact]
+ public void TestSeekBeyondCapacityNoResizeThrows()
+ {
+ Assert.Throws(
+ () =>
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+ writer.Seek(20, SeekOrigin.Begin);
+ }
+ );
+ }
+
+ [Fact]
+ public void TestSeekBeyondCapacityWithResize()
+ {
+ var writer = new SpanWriter(10, true);
+
+ var pos = writer.Seek(20, SeekOrigin.Begin);
+
+ Assert.Equal(20, pos);
+ Assert.True(writer.Capacity >= 20);
+ writer.Dispose();
+ }
+
+ [Fact]
+ public void TestEnsureCapacityNoResize()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ writer.EnsureCapacity(10);
+ Assert.Equal(10, writer.Capacity);
+ }
+
+ [Fact]
+ public void TestEnsureCapacityNoResizeThrows()
+ {
+ Assert.Throws(
+ () =>
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+ writer.EnsureCapacity(20);
+ }
+ );
+ }
+
+ [Fact]
+ public void TestEnsureCapacityWithResize()
+ {
+ var writer = new SpanWriter(10, true);
+
+ writer.EnsureCapacity(50);
+
+ Assert.True(writer.Capacity >= 50);
+ writer.Dispose();
+ }
+
+ [Fact]
+ public void TestToSpanWithStackAlloc()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write((byte)0x01);
+ writer.Write((byte)0x02);
+ writer.Write((byte)0x03);
+
+ using var owner = writer.ToSpan();
+
+ Assert.Equal(3, owner.Span.Length);
+ AssertThat.Equal(owner.Span, [0x01, 0x02, 0x03]);
+ }
+
+ [Fact]
+ public void TestToSpanWithRentedBuffer()
+ {
+ var writer = new SpanWriter(10);
+
+ writer.Write((byte)0x01);
+ writer.Write((byte)0x02);
+ writer.Write((byte)0x03);
+
+ using var owner = writer.ToSpan();
+
+ Assert.Equal(3, owner.Span.Length);
+ AssertThat.Equal(owner.Span, [0x01, 0x02, 0x03]);
+ }
+
+ [Fact]
+ public void TestToSpanEmpty()
+ {
+ var writer = new SpanWriter(10);
+
+ using var owner = writer.ToSpan();
+
+ Assert.Equal(0, owner.Span.Length);
+ }
+
+ [Fact]
+ public void TestBytesWritten()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ Assert.Equal(0, writer.BytesWritten);
+
+ writer.Write((byte)0x01);
+ Assert.Equal(1, writer.BytesWritten);
+
+ writer.Write((ushort)0x0203);
+ Assert.Equal(3, writer.BytesWritten);
+
+ writer.Seek(1, SeekOrigin.Begin);
+ Assert.Equal(3, writer.BytesWritten);
+
+ writer.Write((byte)0xFF);
+ Assert.Equal(3, writer.BytesWritten);
+ }
+
+ [Fact]
+ public void TestCapacity()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ Assert.Equal(10, writer.Capacity);
+ }
+
+ [Fact]
+ public void TestRawBuffer()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write((byte)0x01);
+
+ var raw = writer.RawBuffer;
+ Assert.Equal(10, raw.Length);
+ Assert.Equal(0x01, raw[0]);
+ }
+
+ [Fact]
+ public void TestSpan()
+ {
+ Span buffer = stackalloc byte[10];
+ var writer = new SpanWriter(buffer);
+
+ writer.Write((byte)0x01);
+ writer.Write((byte)0x02);
+
+ var span = writer.Span;
+ Assert.Equal(2, span.Length);
+ AssertThat.Equal(span, [0x01, 0x02]);
}
}
diff --git a/Projects/Server.Tests/Tests/Items/ContainerTests.cs b/Projects/Server.Tests/Tests/Items/ContainerTests.cs
index 0b258a717..8c165f5f6 100644
--- a/Projects/Server.Tests/Tests/Items/ContainerTests.cs
+++ b/Projects/Server.Tests/Tests/Items/ContainerTests.cs
@@ -8,12 +8,14 @@ namespace Server.Tests;
[Collection("Sequential Server Tests")]
public class ContainerTests
{
- [Fact]
- public void TestFindItemsByType()
+ [Theory]
+ [InlineData(typeof(Container))]
+ [InlineData(typeof(Item))]
+ public void TestFindItemsByType(Type itemType)
{
var staticSerial = (Serial)0x3;
- var container = new Container((Serial)0x1);
+ var container = itemType.CreateInstance- ((Serial)0x1);
container.AddItem(new Item((Serial)0x2));
container.AddItem(new Static(staticSerial));
@@ -27,18 +29,20 @@ public class ContainerTests
Assert.Equal(staticSerial, staticItem.Serial);
}
- [Fact]
- public void TestFindItemsByTypeNested()
+ [Theory]
+ [InlineData(typeof(Container))]
+ [InlineData(typeof(Item))]
+ public void TestFindItemsByTypeNested(Type itemType)
{
var static1 = new Static((Serial)0x3);
var static2 = new Static((Serial)0x6);
- var container = new Container((Serial)0x1);
+ var container = itemType.CreateInstance
- ((Serial)0x1);
container.AddItem(new Item((Serial)0x2));
- var container2 = new Container((Serial)0x4);
+ var container2 = itemType.CreateInstance
- ((Serial)0x4);
container.AddItem(container2);
- var container3 = new Container((Serial)0x5);
+ var container3 = itemType.CreateInstance
- ((Serial)0x5);
container2.AddItem(container3);
container3.AddItem(static2);
@@ -55,12 +59,14 @@ public class ContainerTests
Assert.Equal(static2, statics[1]);
}
- [Fact]
- public void TestFindItemsByTypeNotMatching()
+ [Theory]
+ [InlineData(typeof(Container))]
+ [InlineData(typeof(Item))]
+ public void TestFindItemsByTypeNotMatching(Type itemType)
{
- var container = new Container((Serial)0x1);
+ var container = itemType.CreateInstance
- ((Serial)0x1);
container.AddItem(new Item((Serial)0x2));
- var container2 = new Container((Serial)0x4);
+ var container2 = itemType.CreateInstance
- ((Serial)0x4);
container.AddItem(container2);
container2.AddItem(new Item((Serial)0x5));
@@ -73,10 +79,12 @@ public class ContainerTests
Assert.Null(staticItem);
}
- [Fact]
- public void TestFindItemsByTypeShouldThrowWhenModified()
+ [Theory]
+ [InlineData(typeof(Container))]
+ [InlineData(typeof(Item))]
+ public void TestFindItemsByTypeShouldThrowWhenModified(Type itemType)
{
- var container = new Container((Serial)0x1);
+ var container = itemType.CreateInstance
- ((Serial)0x1);
container.AddItem(new Item((Serial)0x2));
var staticItem = new Static((Serial)0x3);
container.AddItem(staticItem);
@@ -96,10 +104,12 @@ public class ContainerTests
);
}
- [Fact]
- public void TestEnumerateItemsByTypeWhenModified()
+ [Theory]
+ [InlineData(typeof(Container))]
+ [InlineData(typeof(Item))]
+ public void TestEnumerateItemsByTypeWhenModified(Type itemType)
{
- var container = new Container((Serial)0x1);
+ var container = itemType.CreateInstance
- ((Serial)0x1);
var item1 = new Item((Serial)0x2);
container.AddItem(item1);
diff --git a/Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs
new file mode 100644
index 000000000..b2eb644ea
--- /dev/null
+++ b/Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs
@@ -0,0 +1,549 @@
+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);
+ }
+
+ [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++)
+ {
+ 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/ItemByDistanceEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/ItemByDistanceEnumeratorTests.cs
new file mode 100644
index 000000000..fe19c76fb
--- /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(World.NewItem);
+ var testItem = new TestItem(World.NewItem);
+
+ 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(World.NewItem);
+ 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
new file mode 100644
index 000000000..1a9495008
--- /dev/null
+++ b/Projects/Server.Tests/Tests/Maps/ItemEnumeratorTests.cs
@@ -0,0 +1,484 @@
+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);
+ }
+ }
+
+ [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);
+ 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/MobileByDistanceEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/MobileByDistanceEnumeratorTests.cs
new file mode 100644
index 000000000..0a84da9e6
--- /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(World.NewMobile);
+ var npc = new TestMobile(World.NewMobile);
+
+ 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(World.NewMobile);
+ 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
new file mode 100644
index 000000000..5df3d4b69
--- /dev/null
+++ b/Projects/Server.Tests/Tests/Maps/MobileEnumeratorTests.cs
@@ -0,0 +1,263 @@
+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);
+ }
+ }
+
+ [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(World.NewMobile);
+ 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..d99f7bbad
--- /dev/null
+++ b/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs
@@ -0,0 +1,412 @@
+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 (InvalidOperationException)
+ {
+ 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);
+ }
+ }
+
+ [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();
+ 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