Merge branch 'main' into Refactor/MagerySpell.cs

This commit is contained in:
Kamron Batman 2025-11-29 10:22:05 -08:00 committed by GitHub
commit adbb6e272f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
712 changed files with 17498 additions and 9146 deletions

View file

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"modernuoschemagenerator": {
"version": "2.12.20",
"version": "2.13.0",
"commands": [
"ModernUOSchemaGenerator"
]

View file

@ -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

View file

@ -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

View file

@ -3,9 +3,9 @@
<PropertyGroup>
<Authors>Kamron Batman</Authors>
<Company>ModernUO</Company>
<Copyright>2019-2024</Copyright>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>13</LangVersion>
<Copyright>2019-2025</Copyright>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14</LangVersion>
<PublicRelease>true</PublicRelease>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<NoWarn>NU1603</NoWarn>
@ -62,11 +62,11 @@
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Nerdbank.GitVersioning" Condition="!Exists('packages.config')">
<Version>3.7.115</Version>
<Version>3.9.50</Version>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<AdditionalFiles Include="..\..\Rules.ruleset" />

View file

@ -5,9 +5,9 @@
<RootNamespace>Server.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -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<byte> 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<EndOfStreamException>(
() =>
{
ReadOnlySpan<byte> buffer = [0x12];
var reader = new SpanReader(buffer);
reader.ReadByte();
reader.ReadByte();
}
);
}
[Fact]
public void TestReadBoolean()
{
ReadOnlySpan<byte> 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<byte> 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<byte> buffer = [0x12, 0x34];
var reader = new SpanReader(buffer);
Assert.Equal(0x1234, reader.ReadInt16());
Assert.Equal(2, reader.Position);
}
[Fact]
public void TestReadInt16LittleEndian()
{
ReadOnlySpan<byte> buffer = [0x34, 0x12];
var reader = new SpanReader(buffer);
Assert.Equal(0x1234, reader.ReadInt16LE());
Assert.Equal(2, reader.Position);
}
[Fact]
public void TestReadInt16AtEnd()
{
Assert.Throws<EndOfStreamException>(
() =>
{
ReadOnlySpan<byte> buffer = [0x12];
var reader = new SpanReader(buffer);
reader.ReadInt16();
}
);
}
[Fact]
public void TestReadUInt16BigEndian()
{
ReadOnlySpan<byte> buffer = [0x12, 0x34];
var reader = new SpanReader(buffer);
Assert.Equal((ushort)0x1234, reader.ReadUInt16());
Assert.Equal(2, reader.Position);
}
[Fact]
public void TestReadUInt16LittleEndian()
{
ReadOnlySpan<byte> buffer = [0x34, 0x12];
var reader = new SpanReader(buffer);
Assert.Equal((ushort)0x1234, reader.ReadUInt16LE());
Assert.Equal(2, reader.Position);
}
[Fact]
public void TestReadInt32BigEndian()
{
ReadOnlySpan<byte> 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<EndOfStreamException>(
() =>
{
ReadOnlySpan<byte> buffer = [0x12, 0x34, 0x56];
var reader = new SpanReader(buffer);
reader.ReadInt32();
}
);
}
[Fact]
public void TestReadUInt32BigEndian()
{
ReadOnlySpan<byte> 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<byte> 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<byte> 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<EndOfStreamException>(
() =>
{
ReadOnlySpan<byte> buffer = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE];
var reader = new SpanReader(buffer);
reader.ReadInt64();
}
);
}
[Fact]
public void TestReadUInt64BigEndian()
{
ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> 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<EndOfStreamException>(
() =>
{
ReadOnlySpan<byte> buffer = "Hel"u8;
var reader = new SpanReader(buffer);
reader.ReadAscii(10);
}
);
}
[Fact]
public void TestReadAsciiSafe()
{
ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<ArgumentOutOfRangeException>(
() =>
{
ReadOnlySpan<byte> buffer = [0x01, 0x02, 0x03];
var reader = new SpanReader(buffer);
reader.Seek(-1, SeekOrigin.Begin);
}
);
}
[Fact]
public void TestSeekBeyondEndThrows()
{
Assert.Throws<ArgumentOutOfRangeException>(
() =>
{
ReadOnlySpan<byte> buffer = [0x01, 0x02, 0x03];
var reader = new SpanReader(buffer);
reader.Seek(10, SeekOrigin.Begin);
}
);
}
[Fact]
public void TestRead()
{
ReadOnlySpan<byte> buffer = [0x01, 0x02, 0x03, 0x04, 0x05];
var reader = new SpanReader(buffer);
Span<byte> 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<byte> buffer = [0x01, 0x02, 0x03];
var reader = new SpanReader(buffer);
Span<byte> 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<byte> buffer = [0x01, 0x02, 0x03];
var reader = new SpanReader(buffer);
Span<byte> dest = [];
var bytesRead = reader.Read(dest);
Assert.Equal(0, bytesRead);
Assert.Equal(0, reader.Position);
}
[Fact]
public void TestReadAtEnd()
{
ReadOnlySpan<byte> buffer = [0x01, 0x02, 0x03];
var reader = new SpanReader(buffer);
reader.Seek(3, SeekOrigin.Begin);
Span<byte> dest = stackalloc byte[5];
var bytesRead = reader.Read(dest);
Assert.Equal(0, bytesRead);
Assert.Equal(3, reader.Position);
}
[Fact]
public void TestLength()
{
ReadOnlySpan<byte> buffer = [0x01, 0x02, 0x03, 0x04, 0x05];
var reader = new SpanReader(buffer);
Assert.Equal(5, reader.Length);
}
[Fact]
public void TestPosition()
{
ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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);
}
}

View file

@ -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<byte> smallStack = stackalloc byte[8];
using var writer = new SpanWriter(smallStack, true);
writer.Write(0x1024L);
writer.Write(0x1024L);
Span<byte> 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<byte> 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<byte> 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<OutOfMemoryException>(
() =>
{
Span<byte> 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<InvalidOperationException>(
() =>
{
Span<byte> smallStack = stackalloc byte[8];
var writer = new SpanWriter(smallStack);
writer.Write(0x1024L);
writer.Write(0x1024L);
}
);
}
[Fact]
public void TestWriteBool()
{
Span<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> buffer = stackalloc byte[6];
var writer = new SpanWriter(buffer);
ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<ArgumentOutOfRangeException>(
() =>
{
Span<byte> buffer = stackalloc byte[10];
var writer = new SpanWriter(buffer);
writer.Seek(-1, SeekOrigin.Begin);
}
);
}
[Fact]
public void TestSeekBeyondCapacityNoResizeThrows()
{
Assert.Throws<InvalidOperationException>(
() =>
{
Span<byte> 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<byte> buffer = stackalloc byte[10];
var writer = new SpanWriter(buffer);
writer.EnsureCapacity(10);
Assert.Equal(10, writer.Capacity);
}
[Fact]
public void TestEnsureCapacityNoResizeThrows()
{
Assert.Throws<InvalidOperationException>(
() =>
{
Span<byte> 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<byte> 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<byte> 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<byte> buffer = stackalloc byte[10];
var writer = new SpanWriter(buffer);
Assert.Equal(10, writer.Capacity);
}
[Fact]
public void TestRawBuffer()
{
Span<byte> 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<byte> 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]);
}
}

View file

@ -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<Item>((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<Item>((Serial)0x1);
container.AddItem(new Item((Serial)0x2));
var container2 = new Container((Serial)0x4);
var container2 = itemType.CreateInstance<Item>((Serial)0x4);
container.AddItem(container2);
var container3 = new Container((Serial)0x5);
var container3 = itemType.CreateInstance<Item>((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<Item>((Serial)0x1);
container.AddItem(new Item((Serial)0x2));
var container2 = new Container((Serial)0x4);
var container2 = itemType.CreateInstance<Item>((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<Item>((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<Item>((Serial)0x1);
var item1 = new Item((Serial)0x2);
container.AddItem(item1);

View file

@ -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<NetState>();
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<NetState>();
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<NetState>();
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<NetState>();
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<NetState>();
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<NetState>();
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<NetState>();
foreach (var ns in map.GetClientsAt(location.X, location.Y))
{
found2.Add(ns);
}
// Test Point2D overload
var found3 = new List<NetState>();
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<NetState>();
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<NetState>();
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<int, Mobile> _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<NetState>();
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<NetState>();
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();
}
}
}

View file

@ -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<Item>();
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<Item>();
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<Item>();
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<Item>(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<Item>();
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<TestItem2>();
foreach (var (Item, _) in map.GetItemsInRangeByDistance<TestItem2>(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<Item>();
foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range))
{
found1.Add(Item);
}
// Test (int, int) overload
var found2 = new List<Item>();
foreach (var (Item, _) in map.GetItemsInRangeByDistance<Item>(center.X, center.Y, range))
{
found2.Add(Item);
}
// Test Point2D overload
var found3 = new List<Item>();
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<Item>();
var distances = new List<int>();
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<Item>();
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<Item>();
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<TestItem>();
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<Item>();
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<Item>();
foreach (var (Item, _) in map.GetItemsInBoundsByDistance<Item>(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<Item>();
foreach (var (Item, _) in map.GetItemsInBoundsByDistance<Item>(bounds))
{
foundWithoutInclusive.Add(Item);
}
var foundWithInclusive = new List<Item>();
foreach (var (Item, _) in map.GetItemsInBoundsByDistance<Item>(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<Item>(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<Item>(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)
{
}
}
}

View file

@ -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<Item>();
foreach (var item in map.GetItemsInBounds<Item>(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<Item>();
foreach (var item in map.GetItemsInBounds<Item>(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<Item>();
foreach (var item in map.GetItemsInBounds<Item>(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<Item>(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<Item>(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<Item>(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<Item>();
foreach (var item in map.GetItemsInBounds<Item>(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<Item>(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<Item>();
foreach (var item in map.GetItemsAt<Item>(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<Item>();
foreach (var item in map.GetItemsAt<Item>(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<Item>();
foreach (var item in map.GetItemsAt<Item>(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<Item>(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<Item>(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<Item>();
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<Item>();
foreach (var item in map.GetItemsAt(location.X, location.Y))
{
found2.Add(item);
}
// Test Point2D overload
var found3 = new List<Item>();
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<Item>();
foreach (var item in map.GetItemsInRange<Item>(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<Item>();
foreach (var item in map.GetItemsInRange<Item>(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();
}
}
}

View file

@ -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<Mobile>();
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<Mobile>();
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<Mobile>();
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<Mobile>(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<Mobile>();
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<TestPlayerMobile>();
foreach (var (mobile, _) in map.GetMobilesInRangeByDistance<TestPlayerMobile>(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<Mobile>();
foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range))
{
found1.Add(mobile);
}
// Test (int, int) overload
var found2 = new List<Mobile>();
foreach (var (mobile, _) in map.GetMobilesInRangeByDistance<Mobile>(center.X, center.Y, range))
{
found2.Add(mobile);
}
// Test Point2D overload
var found3 = new List<Mobile>();
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<Mobile>();
var distances = new List<int>();
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<Mobile>();
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<Mobile>();
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<TestMobile>();
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<Mobile>();
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<Mobile>();
foreach (var (mobile, _) in map.GetMobilesInBoundsByDistance<Mobile>(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<Mobile>();
foreach (var (mobile, _) in map.GetMobilesInBoundsByDistance<Mobile>(bounds))
{
foundWithoutInclusive.Add(mobile);
}
var foundWithInclusive = new List<Mobile>();
foreach (var (mobile, _) in map.GetMobilesInBoundsByDistance<Mobile>(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<Mobile>(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<Mobile>(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)
{
}
}
}

View file

@ -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<Mobile>();
foreach (var m in map.GetMobilesInBounds<Mobile>(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<Mobile>();
foreach (var m in map.GetMobilesInBounds<Mobile>(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<Mobile>(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<Mobile>(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<Mobile>(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<Mobile>();
foreach (var m in map.GetMobilesInBounds<Mobile>(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<Mobile>(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<Mobile>();
foreach (var mobile in map.GetMobilesInRange<Mobile>(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<Mobile>();
foreach (var mobile in map.GetMobilesInRange<Mobile>(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();
}
}
}

View file

@ -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<BaseMulti>();
foreach (var multi in map.GetMultisInBounds<BaseMulti>(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<BaseMulti>();
foreach (var multi in map.GetMultisInBounds<BaseMulti>(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<BaseMulti>(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<BaseMulti>(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<BaseMulti>(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<BaseMulti>();
foreach (var multi in map.GetMultisInBounds<BaseMulti>(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<BaseMulti>(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<BaseMulti>();
foreach (var multi in map.GetMultisInRange<BaseMulti>(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<BaseMulti>();
foreach (var multi in map.GetMultisInSector<BaseMulti>(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<BaseMulti>();
foreach (var multi in map.GetMultisInRange<BaseMulti>(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<BaseMulti>(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<BaseMulti>();
foreach (var multi in map.GetMultisInSector(location))
{
found1.Add(multi);
}
// Test (int, int) overload
var found2 = new List<BaseMulti>();
foreach (var multi in map.GetMultisInSector(location.X, location.Y))
{
found2.Add(multi);
}
// Test Point2D overload
var found3 = new List<BaseMulti>();
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<BaseMulti>();
foreach (var multi in map.GetMultisInRange<BaseMulti>(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<BaseMulti>();
foreach (var multi in map.GetMultisInRange<BaseMulti>(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)
{
}
}
}

View file

@ -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<StaticTile>();
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<StaticTile>();
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<StaticTile>();
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<StaticTile>();
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<StaticTile>();
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<StaticTile>();
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<StaticTile>();
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<StaticTile>();
foreach (var tile in map.Tiles.GetMultiTiles(x, y))
{
tiles.Add(tile);
}
// Should include multi tiles
Assert.NotEmpty(tiles);
}
finally
{
multi?.Delete();
}
}
[Fact]
public void StaticTileEnumerator_MultipleMultisAtSameLocation()
{
var map = Map.Felucca;
var location = new Point2D(900, 900);
var multis = new TestMulti[2];
try
{
// Create two multis at the same location
multis[0] = CreateMultiWithComponents(map, new Point3D(900, 900, 0));
multis[1] = CreateMultiWithComponents(map, new Point3D(900, 900, 5));
// Get all multi tiles
var tiles = new List<StaticTile>();
foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: false, includeMultis: true))
{
tiles.Add(tile);
}
// Should include tiles from both multis
Assert.NotEmpty(tiles);
// We expect at least tiles from both multis
Assert.True(tiles.Count >= 2);
}
finally
{
multis[0]?.Delete();
multis[1]?.Delete();
}
}
[Fact]
public void StaticTileEnumerator_EmptyReturnsCorrectly()
{
var enumerator = Map.StaticTileEnumerable.Empty.GetEnumerator();
Assert.False(enumerator.MoveNext());
}
[Fact]
public void StaticTileEnumerator_DeletedMultiSkipped()
{
var map = Map.Felucca;
var location = new Point2D(1000, 1000);
var multis = new TestMulti[2];
try
{
// Create two multis
multis[0] = CreateMultiWithComponents(map, new Point3D(1000, 1000, 0));
multis[1] = CreateMultiWithComponents(map, new Point3D(1000, 1000, 5));
// Delete the first multi
multis[0].Delete();
// Get all multi tiles
var tiles = new List<StaticTile>();
foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: false, includeMultis: true))
{
tiles.Add(tile);
}
// Should only include tiles from the second multi
Assert.NotEmpty(tiles);
Assert.All(tiles, tile => Assert.True(tile.Z >= 5));
}
finally
{
multis[0]?.Delete();
multis[1]?.Delete();
}
}
private static TestMulti CreateMultiWithComponents(Map map, Point3D location)
{
var multi = new TestMulti(World.NewItem);
multi.MoveToWorld(location, map);
return multi;
}
private class TestMulti : BaseMulti
{
public TestMulti(Serial serial) : base(serial)
{
}
public override MultiComponentList Components => DefaultComponents;
private static readonly MultiComponentList DefaultComponents = new(
[
new MultiTileEntry(0x1, 0, 0, 0, 0x0),
new MultiTileEntry(0x2, 1, 0, 0, 0x0)
]
);
}
}

View file

@ -0,0 +1,242 @@
using System;
using System.Linq;
using Xunit;
namespace Server.Tests;
/// <summary>
/// Tests for the Html.EscapeHtml extension methods.
/// These tests verify correct HTML entity escaping and edge cases.
/// </summary>
public class HtmlEscapeTests
{
[Theory(DisplayName = "No escaping needed")]
[InlineData("")]
[InlineData("Hello World")]
[InlineData("Plain text without special characters")]
[InlineData("123456789")]
[InlineData("!@#$%^*()_+-=[]{}|;:,.?")]
public void EscapeHtml_NoSpecialCharacters_ReturnsUnchanged(string input)
{
var result = input.EscapeHtml();
Assert.Equal(input, result);
}
[Theory(DisplayName = "Single character escaping")]
[InlineData("<", "&lt;")]
[InlineData(">", "&gt;")]
[InlineData("&", "&amp;")]
[InlineData("\"", "&quot;")]
[InlineData("'", "&#39;")]
public void EscapeHtml_SingleSpecialCharacter_EscapesCorrectly(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Multiple instances of same character")]
[InlineData("<><>", "&lt;&gt;&lt;&gt;")]
[InlineData("&&&&", "&amp;&amp;&amp;&amp;")]
[InlineData("\"\"\"", "&quot;&quot;&quot;")]
[InlineData("'''", "&#39;&#39;&#39;")]
[InlineData(">>>", "&gt;&gt;&gt;")]
public void EscapeHtml_MultipleSpecialCharacters_EscapesAll(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Mixed content")]
[InlineData("Hello <world>", "Hello &lt;world&gt;")]
[InlineData("<div>Hello</div>", "&lt;div&gt;Hello&lt;/div&gt;")]
[InlineData("Tom & Jerry", "Tom &amp; Jerry")]
[InlineData("He said \"hello\"", "He said &quot;hello&quot;")]
[InlineData("It's a test", "It&#39;s a test")]
public void EscapeHtml_MixedContent_EscapesMixedSpecialCharacters(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Starting with special character")]
[InlineData("<Hello", "&lt;Hello")]
[InlineData(">World", "&gt;World")]
[InlineData("&Start", "&amp;Start")]
[InlineData("\"Quote", "&quot;Quote")]
[InlineData("'Apostrophe", "&#39;Apostrophe")]
public void EscapeHtml_StartsWithSpecialCharacter_EscapesStart(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Ending with special character")]
[InlineData("Hello<", "Hello&lt;")]
[InlineData("World>", "World&gt;")]
[InlineData("End&", "End&amp;")]
[InlineData("Quote\"", "Quote&quot;")]
[InlineData("Test'", "Test&#39;")]
public void EscapeHtml_EndsWithSpecialCharacter_EscapesEnd(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Complex mixed scenarios")]
[InlineData("<p>Hello & goodbye</p>", "&lt;p&gt;Hello &amp; goodbye&lt;/p&gt;")]
[InlineData("&lt;already&gt;", "&amp;lt;already&amp;gt;")]
[InlineData("<tag attr=\"value\" data='test'>", "&lt;tag attr=&quot;value&quot; data=&#39;test&#39;&gt;")]
[InlineData("a<b>c&d\"e'f", "a&lt;b&gt;c&amp;d&quot;e&#39;f")]
[InlineData("&nbsp;", "&amp;nbsp;")]
public void EscapeHtml_ComplexScenarios_EscapesAllSpecialCharacters(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "Null string input")]
public void EscapeHtml_NullString_ReturnsEmpty()
{
string? input = null;
var result = input.EscapeHtml();
Assert.Empty(result);
}
[Fact(DisplayName = "Empty string input")]
public void EscapeHtml_EmptyString_ReturnsEmpty()
{
var result = "".EscapeHtml();
Assert.Empty(result);
}
[Fact(DisplayName = "Only special characters")]
public void EscapeHtml_OnlySpecialCharacters_EscapesAll()
{
const string input = "<>&\"'";
const string expected = "&lt;&gt;&amp;&quot;&#39;";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "Ampersand must be escaped first")]
public void EscapeHtml_AmpersandFirst_PreventDoubleEscaping()
{
// This is critical: & must be escaped to &amp;
// If we're not careful, we could double-escape already-escaped content
const string input = "&lt;";
const string expected = "&amp;lt;";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "ReadOnlySpan overload - no special characters")]
[InlineData("Hello World")]
[InlineData("Plain text")]
public void EscapeHtml_ReadOnlySpan_NoSpecialCharacters_ReturnsUnchanged(string input)
{
var result = input.AsSpan().EscapeHtml();
Assert.Equal(input, result);
}
[Theory(DisplayName = "ReadOnlySpan overload - with special characters")]
[InlineData("<div>", "&lt;div&gt;")]
[InlineData("Tom & Jerry", "Tom &amp; Jerry")]
public void EscapeHtml_ReadOnlySpan_WithSpecialCharacters_EscapesCorrectly(string input, string expected)
{
var result = input.AsSpan().EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "ReadOnlySpan overload - empty input")]
[InlineData("")]
public void EscapeHtml_ReadOnlySpan_Empty_ReturnsEmpty(string input)
{
var result = input.AsSpan().EscapeHtml();
Assert.Empty(result);
}
[Fact(DisplayName = "Consecutive special characters")]
public void EscapeHtml_ConsecutiveSpecialCharacters_EscapesAll()
{
const string input = "<<>>&&\"\"''";
const string expected = "&lt;&lt;&gt;&gt;&amp;&amp;&quot;&quot;&#39;&#39;";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "Special characters with single normal character between")]
public void EscapeHtml_SpecialCharactersWithGaps_EscapesAll()
{
const string input = "<a>b&c\"d'e";
const string expected = "&lt;a&gt;b&amp;c&quot;d&#39;e";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "HTML tags")]
public void EscapeHtml_HtmlTags_EscapesTagBrackets()
{
var input = "<html><body>Hello</body></html>";
var expected = "&lt;html&gt;&lt;body&gt;Hello&lt;/body&gt;&lt;/html&gt;";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "HTML attributes with mixed quotes")]
public void EscapeHtml_HtmlAttributesWithQuotes_EscapesCorrectly()
{
var input = "<a href=\"test\" data='value'>";
var expected = "&lt;a href=&quot;test&quot; data=&#39;value&#39;&gt;";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Whitespace handling")]
[InlineData(" spaces ", " spaces ")]
[InlineData("\ttabs\t", "\ttabs\t")]
[InlineData("\nnewlines\n", "\nnewlines\n")]
public void EscapeHtml_Whitespace_PreservedAsIs(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "Performance: long string without special characters")]
public void EscapeHtml_LongStringNoSpecialCharacters_ReturnsQuickly()
{
var input = new string('a', 10000);
var result = input.EscapeHtml();
Assert.Equal(input, result);
}
[Fact(DisplayName = "Performance: long string with special characters")]
public void EscapeHtml_LongStringWithSpecialCharacters_HandlesCorrectly()
{
var input = $"Start{new string('<', 100)}End{new string('&', 100)}Final";
var expected =
$"Start{string.Join("", Enumerable.Repeat("&lt;", 100))}End{string.Join("", Enumerable.Repeat("&amp;", 100))}Final";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "Unicode characters")]
public void EscapeHtml_UnicodeCharacters_PreservedWithSpecialCharsEscaped()
{
const string input = "Hello 世界 <test> & 🎉";
const string expected = "Hello 世界 &lt;test&gt; &amp; 🎉";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "String overload matches ReadOnlySpan overload")]
public void EscapeHtml_StringVsReadOnlySpan_ProduceSameResult()
{
const string input = "<div>Tom & Jerry 'in' \"quotes\"</div>";
var resultString = input.EscapeHtml();
var resultSpan = input.AsSpan().EscapeHtml();
Assert.Equal(resultString, resultSpan);
}
}

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: SpanReader.cs *
* *
@ -14,7 +14,6 @@
*************************************************************************/
using System.Buffers.Binary;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
@ -45,7 +44,7 @@ public ref struct SpanReader
{
if (Position >= Length)
{
throw new OutOfMemoryException();
throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
return _buffer[Position++];
@ -62,7 +61,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadInt16BigEndian(_buffer[Position..], out var value))
{
throw new OutOfMemoryException();
throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 2;
@ -74,7 +73,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadInt16LittleEndian(_buffer[Position..], out var value))
{
throw new OutOfMemoryException();
throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 2;
@ -86,7 +85,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadUInt16BigEndian(_buffer[Position..], out var value))
{
throw new OutOfMemoryException();
throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 2;
@ -98,7 +97,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadUInt16LittleEndian(_buffer[Position..], out var value))
{
throw new OutOfMemoryException();
throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 2;
@ -110,7 +109,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadInt32BigEndian(_buffer[Position..], out var value))
{
throw new OutOfMemoryException();
throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 4;
@ -122,7 +121,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadUInt32BigEndian(_buffer[Position..], out var value))
{
throw new OutOfMemoryException();
throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 4;
@ -134,7 +133,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadUInt32LittleEndian(_buffer[Position..], out var value))
{
throw new OutOfMemoryException();
throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 4;
@ -146,7 +145,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadInt64BigEndian(_buffer[Position..], out var value))
{
throw new OutOfMemoryException();
throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 8;
@ -158,7 +157,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadUInt64BigEndian(_buffer[Position..], out var value))
{
throw new OutOfMemoryException();
throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 8;
@ -173,9 +172,9 @@ public ref struct SpanReader
return "";
}
int byteLength = encoding.GetByteLengthForEncoding();
var byteLength = encoding.GetByteLengthForEncoding();
bool isFixedLength = fixedLength > -1;
var isFixedLength = fixedLength > -1;
var remaining = Remaining;
int size;
@ -184,7 +183,7 @@ public ref struct SpanReader
size = fixedLength * byteLength;
if (size > Remaining)
{
throw new OutOfMemoryException();
throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
}
else
@ -255,46 +254,28 @@ public ref struct SpanReader
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Seek(int offset, SeekOrigin origin)
{
Debug.Assert(
origin != SeekOrigin.End || offset <= 0,
"Attempting to seek to a position beyond capacity using SeekOrigin.End"
);
Debug.Assert(
origin != SeekOrigin.End || offset >= -_buffer.Length,
"Attempting to seek to a negative position using SeekOrigin.End"
);
Debug.Assert(
origin != SeekOrigin.Begin || offset >= 0,
"Attempting to seek to a negative position using SeekOrigin.Begin"
);
Debug.Assert(
origin != SeekOrigin.Begin || offset <= _buffer.Length,
"Attempting to seek to a position beyond the capacity using SeekOrigin.Begin"
);
Debug.Assert(
origin != SeekOrigin.Current || Position + offset >= 0,
"Attempting to seek to a negative position using SeekOrigin.Current"
);
Debug.Assert(
origin != SeekOrigin.Current || Position + offset <= _buffer.Length,
"Attempting to seek to a position beyond the capacity using SeekOrigin.Current"
);
return Position = Math.Max(0, origin switch
var newPosition = origin switch
{
SeekOrigin.Current => Position + offset,
SeekOrigin.End => _buffer.Length + offset,
_ => offset // Begin
});
};
if (newPosition < 0)
{
throw new ArgumentOutOfRangeException(nameof(offset), "Seek operation would result in a negative position.");
}
if (newPosition > _buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset), $"Cannot seek to position {newPosition} beyond buffer length {_buffer.Length}.");
}
return Position = newPosition;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Read(Span<byte> bytes)
public int Read(scoped Span<byte> bytes)
{
if (bytes.Length == 0)
{

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: SpanWriter.cs *
* *
@ -14,7 +14,6 @@
*************************************************************************/
using System.Buffers.Binary;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -56,14 +55,14 @@ public ref struct SpanWriter
public Span<byte> RawBuffer => _buffer;
/**
* Converts the writer to a Span<byte> using a SpanOwner.
* If the buffer was stackalloc, it will be copied to a rented buffer.
* Otherwise the existing rented buffer is used.
*
* Note:
* Do not use the SpanWriter after calling this method.
* This method will effectively dispose of the SpanWriter and is therefore considered terminal.
*/
* Converts the writer to a Span<byte> using a SpanOwner.
* If the buffer was stackalloc, it will be copied to a rented buffer.
* Otherwise the existing rented buffer is used.
*
* Note:
* Do not use the SpanWriter after calling this method.
* This method will effectively dispose of the SpanWriter and is therefore considered terminal.
*/
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SpanOwner ToSpan()
{
@ -117,11 +116,11 @@ public ref struct SpanWriter
private void Grow(int additionalCapacity)
{
var newSize = Math.Max(BytesWritten + additionalCapacity, _buffer.Length * 2);
byte[] poolArray = STArrayPool<byte>.Shared.Rent(newSize);
var poolArray = STArrayPool<byte>.Shared.Rent(newSize);
_buffer[..BytesWritten].CopyTo(poolArray);
byte[] toReturn = _arrayToReturnToPool;
var toReturn = _arrayToReturnToPool;
_buffer = _arrayToReturnToPool = poolArray;
if (toReturn != null)
{
@ -136,7 +135,7 @@ public ref struct SpanWriter
{
if (!_resize)
{
throw new OutOfMemoryException();
throw new InvalidOperationException("Buffer is full and resizing is disabled.");
}
Grow(count);
@ -151,7 +150,7 @@ public ref struct SpanWriter
{
if (!_resize)
{
throw new OutOfMemoryException();
throw new InvalidOperationException("Buffer is full and resizing is disabled.");
}
Grow(capacity - BytesWritten);
@ -400,46 +399,25 @@ public ref struct SpanWriter
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Seek(int offset, SeekOrigin origin)
{
Debug.Assert(
origin != SeekOrigin.End || _resize || offset <= 0,
"Attempting to seek to a position beyond capacity using SeekOrigin.End without resize"
);
Debug.Assert(
origin != SeekOrigin.End || offset >= -_buffer.Length,
"Attempting to seek to a negative position using SeekOrigin.End"
);
Debug.Assert(
origin != SeekOrigin.Begin || offset >= 0,
"Attempting to seek to a negative position using SeekOrigin.Begin"
);
Debug.Assert(
origin != SeekOrigin.Begin || _resize || offset <= _buffer.Length,
"Attempting to seek to a position beyond the capacity using SeekOrigin.Begin without resize"
);
Debug.Assert(
origin != SeekOrigin.Current || _position + offset >= 0,
"Attempting to seek to a negative position using SeekOrigin.Current"
);
Debug.Assert(
origin != SeekOrigin.Current || _resize || _position + offset <= _buffer.Length,
"Attempting to seek to a position beyond the capacity using SeekOrigin.Current without resize"
);
var newPosition = Math.Max(0, origin switch
var newPosition = origin switch
{
SeekOrigin.Current => _position + offset,
SeekOrigin.End => BytesWritten + offset,
_ => offset // Begin
});
};
if (newPosition < 0)
{
throw new ArgumentOutOfRangeException(nameof(offset), "Seek operation would result in a negative position.");
}
if (newPosition > _buffer.Length)
{
if (!_resize)
{
throw new InvalidOperationException($"Cannot seek to position {newPosition} beyond buffer capacity {_buffer.Length} when resizing is disabled.");
}
Grow(newPosition - _buffer.Length + 1);
}
@ -449,7 +427,7 @@ public ref struct SpanWriter
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
byte[] toReturn = _arrayToReturnToPool;
var toReturn = _arrayToReturnToPool;
this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
if (toReturn != null)
{
@ -478,7 +456,7 @@ public ref struct SpanWriter
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
byte[] toReturn = _arrayToReturnToPool;
var toReturn = _arrayToReturnToPool;
this = default;
if (_length > 0)
{

View file

@ -1,503 +0,0 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: BaseMulti.SectorMultiLinkList.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Runtime.CompilerServices;
using Server.Collections;
namespace Server.Items;
// Adds support for the specific value link list on sectors for multis, separate from items
public partial class BaseMulti
{
// Sectors, specifically for multis
public BaseMulti SectorMultiNext { get; set; }
public BaseMulti SectorMultiPrevious { get; set; }
public bool OnSectorMultiLinkList { get; set; }
}
public struct SectorMultiValueLinkList
{
public int Count { get; internal set; }
internal BaseMulti _first;
internal BaseMulti _last;
public int Version { get; private set; }
public void Remove(BaseMulti node)
{
if (node == null)
{
return;
}
if (!node.OnSectorMultiLinkList)
{
throw new ArgumentException("Attempted to remove a node that is not on the list.");
}
if (node.SectorMultiPrevious == null)
{
// If SectorMultiPrevious is null, then it is the first element.
if (_first != node)
{
throw new ArgumentException("Attempted to remove a node that is not on the list.");
}
if (_first == _last)
{
_last = null;
_first = null;
}
else
{
_first = node.SectorMultiNext;
}
if (node.SectorMultiNext != null)
{
node.SectorMultiNext.SectorMultiPrevious = null;
}
}
else
{
node.SectorMultiPrevious.SectorMultiNext = node.SectorMultiNext;
// If SectorMultiNext is null, then it is the last element.
if (node.SectorMultiNext == null)
{
_last = node.SectorMultiPrevious;
}
else
{
node.SectorMultiNext.SectorMultiPrevious = node.SectorMultiPrevious;
}
}
node.SectorMultiNext = null;
node.SectorMultiPrevious = null;
node.OnSectorMultiLinkList = false;
Count--;
Version++;
if (Count < 0)
{
throw new Exception("Count is negative!");
}
}
// Remove all entries before this node, not including this node.
public void RemoveAllBefore(BaseMulti e)
{
if (e == null)
{
return;
}
if (!e.OnSectorMultiLinkList)
{
throw new ArgumentException("Attempted to remove nodes before a node that is not on the list.");
}
if (e.SectorMultiPrevious == null)
{
return;
}
var current = e.SectorMultiPrevious;
e.SectorMultiPrevious = null;
while (current != null)
{
var SectorMultiPrevious = current.SectorMultiPrevious;
current.OnSectorMultiLinkList = false;
current.SectorMultiNext = null;
current.SectorMultiPrevious = null;
Count--;
if (Count < 0)
{
throw new Exception("Count is negative!");
}
current = SectorMultiPrevious;
}
_first = e;
Version++;
}
// Remove all entries after this node, not including this node.
public void RemoveAllAfter(BaseMulti e)
{
if (e == null)
{
return;
}
if (!e.OnSectorMultiLinkList)
{
throw new ArgumentException("Attempted to remove nodes after a node that is not on the list.");
}
if (e.SectorMultiNext == null)
{
return;
}
var current = e.SectorMultiNext;
e.SectorMultiNext = null;
while (current != null)
{
var SectorMultiNext = current.SectorMultiNext;
current.OnSectorMultiLinkList = false;
current.SectorMultiNext = null;
current.SectorMultiPrevious = null;
Count--;
if (Count < 0)
{
throw new Exception("Count is negative!");
}
current = SectorMultiNext;
}
_last = e;
Version++;
}
public void AddLast(BaseMulti e)
{
if (e == null)
{
return;
}
if (e.OnSectorMultiLinkList)
{
throw new ArgumentException("Attempted to add a node that is already on a list.");
}
if (_last != null)
{
AddAfter(_last, e);
}
else
{
_first = e;
_last = e;
Count = 1;
Version++;
e.OnSectorMultiLinkList = true;
}
}
public void AddFirst(BaseMulti e)
{
if (e == null)
{
return;
}
if (e.OnSectorMultiLinkList)
{
throw new ArgumentException("Attempted to add a node that is already on a list.");
}
if (_first != null)
{
AddBefore(_first, e);
}
else
{
_first = e;
_last = e;
Count = 1;
Version++;
e.OnSectorMultiLinkList = true;
}
}
public void AddBefore(BaseMulti existing, BaseMulti node)
{
if (node == null)
{
return;
}
ArgumentNullException.ThrowIfNull(existing);
if (!existing.OnSectorMultiLinkList)
{
throw new ArgumentException($"Argument '{nameof(existing)}' must be a node on a list.");
}
if (node.OnSectorMultiLinkList)
{
throw new ArgumentException("Attempted to add a node that is already on a list.");
}
node.SectorMultiNext = existing;
node.SectorMultiPrevious = existing.SectorMultiPrevious;
if (existing.SectorMultiPrevious != null)
{
existing.SectorMultiPrevious.SectorMultiNext = node;
}
else
{
_first = node;
}
existing.SectorMultiPrevious = node;
node.OnSectorMultiLinkList = true;
Count++;
Version++;
}
public void AddAfter(BaseMulti existing, BaseMulti node)
{
if (node == null)
{
return;
}
ArgumentNullException.ThrowIfNull(existing);
if (!existing.OnSectorMultiLinkList)
{
throw new ArgumentException($"Argument '{nameof(existing)}' must be a node on a list.");
}
if (node.OnSectorMultiLinkList)
{
throw new ArgumentException("Attempted to add a node that is already on a list.");
}
node.SectorMultiPrevious = existing;
node.SectorMultiNext = existing.SectorMultiNext;
if (existing.SectorMultiNext != null)
{
existing.SectorMultiNext.SectorMultiPrevious = node;
}
else
{
_last = node;
}
existing.SectorMultiNext = node;
node.OnSectorMultiLinkList = true;
Count++;
Version++;
}
public void RemoveAll()
{
var current = _first;
while (current != null)
{
var SectorMultiNext = current.SectorMultiNext;
current.OnSectorMultiLinkList = false;
current.SectorMultiNext = null;
current.SectorMultiPrevious = null;
current = SectorMultiNext;
}
_first = null;
_last = null;
Count = 0;
Version++;
}
public void AddLast(ref SectorMultiValueLinkList otherList, BaseMulti start, BaseMulti end)
{
// Should we check if start and end actually exist on the other list?
if (otherList.Count == 0 || otherList.Count == 1 && (start != end || otherList._first != start))
{
throw new ArgumentException("Attempted to add nodes that are not on the specified linklist.");
}
if (start.SectorMultiPrevious != null)
{
start.SectorMultiPrevious.SectorMultiNext = end.SectorMultiNext;
}
else
{
// Start is first
otherList._first = end.SectorMultiNext;
}
if (end.SectorMultiNext != null)
{
end.SectorMultiNext.SectorMultiPrevious = start.SectorMultiPrevious;
}
else
{
otherList._last = start.SectorMultiPrevious;
}
var count = 1;
var current = start;
// Assume start and end are in the right order, or bad things happen (crash).
while (current != end)
{
count++;
current = current.SectorMultiNext;
}
otherList.Count -= count;
if (otherList.Count < 0)
{
throw new Exception("Count is negative!");
}
if (_last != null)
{
_last.SectorMultiNext = start;
start.SectorMultiPrevious = _last;
}
else
{
_first = start;
}
_last = end;
Count += count;
Version++;
}
public BaseMulti[] ToArray()
{
var arr = new BaseMulti[Count];
var index = 0;
foreach (var t in this)
{
arr[index++] = t;
}
return arr;
}
public ref struct SectorMultiValueListEnumerator
{
private bool _started;
private BaseMulti _current;
private ref readonly SectorMultiValueLinkList _linkList;
private int _version;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SectorMultiValueListEnumerator(in SectorMultiValueLinkList linkList)
{
_linkList = ref linkList;
_started = false;
_current = null;
_version = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (!_started)
{
_current = _linkList._first;
_started = true;
_version = _linkList.Version;
}
else if (_linkList.Version != _version)
{
throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion);
}
else
{
_current = _current.SectorMultiNext;
}
return _current != null;
}
public BaseMulti Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _current;
}
}
public ref struct DescendingSectorMultiValueListEnumerator
{
private bool _started;
private BaseMulti _current;
private ref readonly SectorMultiValueLinkList _linkList;
private int _version;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DescendingSectorMultiValueListEnumerator(in SectorMultiValueLinkList linkList)
{
_linkList = ref linkList;
_started = false;
_current = null;
_version = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (!_started)
{
_current = _linkList._last;
_started = true;
_version = _linkList.Version;
}
else if (_linkList.Version != _version)
{
throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion);
}
else
{
_current = _current.SectorMultiPrevious;
}
return _current != null;
}
public BaseMulti Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _current;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DescendingSectorMultiValueListEnumerator GetEnumerator() => this;
}
}
public static class SectorMultiValueLinkListExt
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SectorMultiValueLinkList.SectorMultiValueListEnumerator GetEnumerator(this in SectorMultiValueLinkList linkList)
=> new(in linkList);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SectorMultiValueLinkList.DescendingSectorMultiValueListEnumerator ByDescending(this in SectorMultiValueLinkList linkList)
=> new(in linkList);
}

View file

@ -1,3 +1,18 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Container.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
@ -25,7 +40,7 @@ public partial class Container : Item
private int m_TotalItems;
private int m_TotalWeight;
private int _version;
internal int _version;
[SerializableField(3)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
@ -281,16 +296,6 @@ public partial class Container : Item
return true;
}
private static void SetSaveFlag(ref SaveFlag flags, SaveFlag toSet, bool setIf)
{
if (setIf)
{
flags |= toSet;
}
}
private static bool GetSaveFlag(SaveFlag flags, SaveFlag toGet) => (flags & toGet) != 0;
[AfterDeserialization]
private void AfterDeserialization()
{
@ -425,8 +430,8 @@ public partial class Container : Item
public virtual bool TryDropItems(Mobile from, bool sendFullMessage, params ReadOnlySpan<Item> droppedItems)
{
var dropItems = new List<Item>();
var stackItems = new List<ItemStackEntry>();
using var dropItems = PooledRefQueue<Item>.Create();
using var stackItems = PooledRefQueue<ItemStackEntry>.Create();
var extraItems = 0;
var extraWeight = 0;
@ -446,7 +451,7 @@ public partial class Container : Item
if (item is not Container && CheckHold(from, dropped, false, false, 0, extraWeight) &&
item.CanStackWith(dropped))
{
stackItems.Add(new ItemStackEntry(item, dropped));
stackItems.Enqueue(new ItemStackEntry(item, dropped));
extraWeight += (int)Math.Ceiling(item.Weight * (item.Amount + dropped.Amount)) -
item.PileWeight; // extra weight delta, do not need TotalWeight as we do not have hybrid stackable container types
stacked = true;
@ -456,7 +461,7 @@ public partial class Container : Item
if (!stacked && CheckHold(from, dropped, false, true, extraItems, extraWeight))
{
dropItems.Add(dropped);
dropItems.Enqueue(dropped);
extraItems++;
extraWeight += dropped.TotalWeight + dropped.PileWeight;
}
@ -464,14 +469,15 @@ public partial class Container : Item
if (dropItems.Count + stackItems.Count == droppedItems.Length) // All good
{
for (var i = 0; i < dropItems.Count; i++)
while (dropItems.Count > 0)
{
DropItem(dropItems[i]);
DropItem(dropItems.Dequeue());
}
for (var i = 0; i < stackItems.Count; i++)
while (stackItems.Count > 0)
{
stackItems[i].m_StackItem.StackWith(from, stackItems[i].m_DropItem, false);
var stackItem = stackItems.Dequeue();
stackItem.m_StackItem.StackWith(from, stackItem.m_DropItem, false);
}
return true;

View file

@ -1,8 +1,8 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Container.Enumerable.cs *
* File: Item.Enumerable.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
@ -18,13 +18,13 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Server.Collections;
namespace Server.Items;
namespace Server;
public partial class Container
public partial class Item
{
/// <summary>
/// Performs a breadth-first search through all the <see cref="Item" />s and
/// nested <see cref="Container" />s within this <see cref="Container" />.
/// nested <see cref="Item" />s within this <see cref="Item" />.
/// </summary>
/// <remarks>
/// DO NOT consume, delete, or move items while iterating with any FindItemByType or FindItems overloads
@ -42,8 +42,8 @@ public partial class Container
/// <typeparam name="T">Type of objects being searched for</typeparam>
/// <param name="recurse">
/// Optional: If true, the search will recursively
/// check any nested <see cref="Container" />s; otherwise, nested
/// <see cref="Container" />s will not be searched.
/// check any nested <see cref="Item" />s; otherwise, nested
/// <see cref="Item" />s will not be searched.
/// </param>
/// <param name="predicate">
/// Optional: A predicate to check if the <see cref="Item" />
@ -71,7 +71,7 @@ public partial class Container
/// <summary>
/// Safely enumerates items using a breadth-first search through all the <see cref="Item" />s and
/// nested <see cref="Container" />s within this <see cref="Container" />.
/// nested <see cref="Item" />s within this <see cref="Item" />.
/// </summary>
/// <remarks>
/// Use EnumerateItemsByType for situations where the item might be manipulated, consumed, or moved.
@ -92,8 +92,8 @@ public partial class Container
/// <typeparam name="T">Type of objects being searched for</typeparam>
/// <param name="recurse">
/// Optional: If true, the search will recursively
/// check any nested <see cref="Container" />s; otherwise, nested
/// <see cref="Container" />s will not be searched.
/// check any nested <see cref="Item" />s; otherwise, nested
/// <see cref="Item" />s will not be searched.
/// </param>
/// <param name="predicate">
/// Optional: A predicate to check if the <see cref="Item" />
@ -203,30 +203,31 @@ public partial class Container
public ref struct FindItemsByTypeEnumerator<T> where T : Item
{
private const string InvalidOperation_EnumFailedVersion =
"Container was modified after enumerator was instantiated. Use Container.EnumerateItems method instead for safe enumerations.";
"Item was modified after enumerator was instantiated. Use Item.EnumerateItems method instead for safe enumerations.";
private PooledRefQueue<Container> _containers;
private PooledRefQueue<Item> _containers;
private Span<Item> _items;
private int _index;
private T _current;
private readonly bool _recurse;
private readonly Predicate<T> _predicate;
private Container _currentContainer;
private Item _currentContainer;
private int _version;
public FindItemsByTypeEnumerator(Container container, bool recurse, Predicate<T> predicate)
public FindItemsByTypeEnumerator(Item container, bool recurse, Predicate<T> predicate)
{
_containers = PooledRefQueue<Container>.Create(_recurse ? 64 : 0);
_containers = PooledRefQueue<Item>.Create(_recurse ? 64 : 0);
if (container != null)
{
if (container.m_Items != null)
var items = container.LookupItems();
if (items != null)
{
_items = CollectionsMarshal.AsSpan(container.m_Items);
_items = CollectionsMarshal.AsSpan(items);
}
_currentContainer = container;
_version = container._version;
_version = container.LookupContainerVersion();
}
_current = default;
@ -244,9 +245,9 @@ public partial class Container
while (_containers.TryDequeue(out var c))
{
_currentContainer = c;
_items = CollectionsMarshal.AsSpan(c.m_Items);
_items = CollectionsMarshal.AsSpan(c.LookupItems());
_index = 0;
_version = c._version;
_version = c.LookupContainerVersion();
if (SetNextItem())
{
@ -260,7 +261,7 @@ public partial class Container
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool SetNextItem()
{
if (_version != _currentContainer._version)
if (_version != _currentContainer.LookupContainerVersion())
{
throw new InvalidOperationException(InvalidOperation_EnumFailedVersion);
}
@ -268,14 +269,14 @@ public partial class Container
while (_index < _items.Length)
{
Item item = _items[_index++];
if (_recurse && item is Container { m_Items.Count: > 0 } c)
if (_recurse && item.LookupItems() is { Count: > 0 } items)
{
_containers.Enqueue(c);
_containers.Enqueue(item);
}
if (item is T t && _predicate?.Invoke(t) != false)
{
if (_version != _currentContainer._version)
if (_version != _currentContainer.LookupContainerVersion())
{
throw new InvalidOperationException(InvalidOperation_EnumFailedVersion);
}

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Item.cs *
* *
@ -192,7 +192,7 @@ public enum ExpandFlag
Spawner = 0x100
}
public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEntity, IValueLinkListNode<Item>
public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEntity, IValueLinkListNode<Item>
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(Item));
@ -534,6 +534,8 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
public List<Item> Items => LookupItems() ?? EmptyItems;
public int LookupContainerVersion() => (this as Container)?._version ?? LookupCompactInfo()?.Version ?? 0;
[CommandProperty(AccessLevel.GameMaster)]
public IEntity RootParent
{
@ -1696,11 +1698,11 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
{
if (this is Container cont)
{
return cont.m_Items ?? (cont.m_Items = new List<Item>());
return cont.m_Items ??= new List<Item>();
}
var info = AcquireCompactInfo();
return info.m_Items ?? (info.m_Items = new List<Item>());
return info.m_Items ??= new List<Item>();
}
private void SetFlag(ImplFlag flag, bool value)
@ -2471,15 +2473,6 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SetSaveFlag(ref SaveFlag flags, SaveFlag toSet, bool setIf)
{
if (setIf)
{
flags |= toSet;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool GetSaveFlag(SaveFlag flags, SaveFlag toGet) => (flags & toGet) != 0;
@ -3195,9 +3188,13 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
item.Map = m_Map;
var items = AcquireItems();
items.Add(item);
if (this is not Container)
{
AcquireCompactInfo().Version++;
}
if (!item.IsVirtualItem)
{
UpdateTotal(item, TotalType.Gold, item.TotalGold);
@ -3360,6 +3357,11 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
if (items.Remove(item))
{
if (this is not Container)
{
AcquireCompactInfo().Version++;
}
item.SendRemovePacket();
if (!item.IsVirtualItem)
@ -4321,6 +4323,8 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
public int m_TempFlags;
public double m_Weight = -1;
public int Version;
}
[Flags]

View file

@ -0,0 +1,299 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Map.ClientByDistanceEnumerator.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Runtime.CompilerServices;
using Server.Collections;
using Server.Network;
namespace Server;
public partial class Map
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientDistanceEnumerable GetClientsInRangeByDistance(Point3D p) =>
GetClientsInRangeByDistance(p, Core.GlobalMaxUpdateRange);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientDistanceEnumerable GetClientsInRangeByDistance(Point3D p, int range) =>
GetClientsInRangeByDistance(p.m_X, p.m_Y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientDistanceEnumerable GetClientsInRangeByDistance(Point2D p) =>
GetClientsInRangeByDistance(p, Core.GlobalMaxUpdateRange);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientDistanceEnumerable GetClientsInRangeByDistance(Point2D p, int range) =>
GetClientsInRangeByDistance(p.m_X, p.m_Y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientDistanceEnumerable GetClientsInRangeByDistance(int x, int y, int range)
{
var clampedRange = Math.Max(0, range);
var edge = clampedRange * 2 + 1;
return GetClientsInBoundsByDistance(
new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge),
new Point2D(x, y)
);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientDistanceEnumerable GetClientsInBoundsByDistance(Rectangle2D bounds, bool makeBoundsInclusive = false) =>
GetClientsInBoundsByDistance(bounds, new Point2D(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2), makeBoundsInclusive);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ClientDistanceEnumerable GetClientsInBoundsByDistance(
Rectangle2D bounds, Point2D center, bool makeBoundsInclusive = false
) => new(this, bounds, center, makeBoundsInclusive);
public ref struct ClientDistanceEnumerable
{
private readonly Map _map;
private readonly Rectangle2D _bounds;
private readonly Point2D _center;
private readonly bool _makeBoundsInclusive;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientDistanceEnumerable(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive)
{
_map = map;
_bounds = bounds;
_center = center;
_makeBoundsInclusive = makeBoundsInclusive;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientDistanceEnumerator GetEnumerator() => new(_map, _bounds, _center, _makeBoundsInclusive);
}
public ref struct ClientDistanceEnumerator
{
private Map _map;
private Point2D _center;
private Rectangle2D _bounds;
private int _sectorStartX;
private int _maxRing;
private int _ring; // -1 = uninitialized, then 0.._maxRing
private int _ringIndex; // Current index within the ring
private int _currentSectorX;
private int _currentSectorY;
private ref readonly ValueLinkList<NetState> _linkList;
private int _currentVersion;
private NetState _current;
private int _minDistance;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientDistanceEnumerator(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive)
{
_map = map;
_center = center;
_bounds = makeBoundsInclusive
? new Rectangle2D(bounds.X, bounds.Y, bounds.Width + 1, bounds.Height + 1)
: bounds;
_current = null;
if (map != null)
{
var centerSectorX = center.m_X / SectorSize;
var centerSectorY = center.m_Y / SectorSize;
map.CalculateSectors(_bounds, out _sectorStartX, out var sectorStartY, out var sectorEndX, out var sectorEndY);
// Calculate max ring based on bounds
var dx = Math.Max(centerSectorX - _sectorStartX, sectorEndX - centerSectorX);
var dy = Math.Max(centerSectorY - sectorStartY, sectorEndY - centerSectorY);
_maxRing = Math.Max(dx, dy);
}
_ring = -1;
_ringIndex = -1;
_currentSectorX = 0;
_currentSectorY = 0;
_currentVersion = 0;
_minDistance = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
var map = _map;
if (map == null)
{
return false;
}
if (!Unsafe.IsNullRef(in _linkList) && _linkList.Version != _currentVersion)
{
throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion);
}
NetState current = _current;
while (true)
{
current = current?.Next;
while (current == null)
{
while (!TryNextSectorInRing(out _currentSectorX, out _currentSectorY))
{
// Current ring exhausted, try next ring
if (_ring >= _maxRing)
{
return false; // No more rings to search
}
_ring++;
_ringIndex = -1;
}
_linkList = ref map.GetRealSector(_currentSectorX, _currentSectorY).Clients;
_currentVersion = _linkList.Version;
current = _linkList._first;
if (current != null)
{
_minDistance = MinDistToSectorSqrt(_center.m_X, _center.m_Y, _currentSectorX, _currentSectorY);
}
}
var m = current.Mobile;
if (m?.Deleted == false && _bounds.Contains(m.Location))
{
_current = current;
return true;
}
}
}
public (NetState Value, int MinDistance) Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (_current, _minDistance);
}
private bool TryNextSectorInRing(out int sx, out int sy)
{
if (_ring == 0)
{
// Center sector
if (_ringIndex < 0)
{
_ringIndex = 0;
sx = _center.m_X / SectorSize;
sy = _center.m_Y / SectorSize;
return sx >= _sectorStartX;
}
sx = sy = 0;
return false;
}
var totalSectors = _ring * 8;
// Keep trying sectors in this ring until we find a valid one or exhaust the ring
while (true)
{
var nextIndex = _ringIndex + 1;
if (nextIndex >= totalSectors)
{
sx = sy = 0;
return false;
}
_ringIndex = nextIndex;
CalculatePositionFromIndex(nextIndex, out sx, out sy);
if (sx >= _sectorStartX)
{
return true;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CalculatePositionFromIndex(int index, out int x, out int y)
{
var centerSectorX = _center.m_X / SectorSize;
var centerSectorY = _center.m_Y / SectorSize;
var ringSize = _ring * 2;
var startX = centerSectorX - _ring;
var startY = centerSectorY - _ring;
if (index <= ringSize) // Top edge
{
x = startX + index;
y = startY;
}
else if (index <= ringSize * 2) // Right edge
{
x = startX + ringSize;
y = startY + (index - ringSize);
}
else if (index <= ringSize * 3) // Bottom edge
{
x = startX + ringSize - (index - ringSize * 2);
y = startY + ringSize;
}
else // Left edge
{
x = startX;
y = startY + ringSize - (index - ringSize * 3);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int MinDistToSectorSqrt(int cx, int cy, int sectorX, int sectorY)
{
var x0 = sectorX * SectorSize;
var y0 = sectorY * SectorSize;
var x1 = x0 + (SectorSize - 1);
var y1 = y0 + (SectorSize - 1);
var dx = 0;
if (cx < x0)
{
dx = x0 - cx;
}
else if (cx > x1)
{
dx = cx - x1;
}
var dy = 0;
if (cy < y0)
{
dy = y0 - cy;
}
else if (cy > y1)
{
dy = cy - y1;
}
return (int)Math.Sqrt(dx * dx + dy * dy);
}
}
}

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Map.ClientEnumerator.cs *
* *
@ -46,8 +46,12 @@ public partial class Map
GetClientsInRange(p.m_X, p.m_Y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientBoundsEnumerable GetClientsInRange(int x, int y, int range) =>
GetClientsInBounds(new Rectangle2D(x - range, y - range, range * 2 + 1, range * 2 + 1));
public ClientBoundsEnumerable GetClientsInRange(int x, int y, int range)
{
var clampedRange = Math.Max(0, range);
var edge = clampedRange * 2 + 1;
return GetClientsInBounds(new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientBoundsEnumerable GetClientsInBounds(Rectangle2D bounds, bool makeBoundsInclusive = false) =>
@ -76,6 +80,7 @@ public partial class Map
public ref struct ClientAtEnumerator
{
private readonly Map _map;
private bool _started;
private Point2D _location;
private ref readonly ValueLinkList<NetState> _linkList;
@ -85,9 +90,15 @@ public partial class Map
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClientAtEnumerator(Map map, Point2D loc)
{
_map = map;
_started = false;
_location = loc;
_linkList = ref map.GetSector(loc.m_X, loc.m_Y).Clients;
if (map != null)
{
_linkList = ref map.GetSector(loc.m_X, loc.m_Y).Clients;
}
_version = 0;
_current = null;
}
@ -95,6 +106,11 @@ public partial class Map
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_map == null)
{
return false;
}
ref var loc = ref _location;
NetState current;
Mobile m;
@ -105,7 +121,7 @@ public partial class Map
_started = true;
_version = _linkList.Version;
m = current.Mobile;
m = current?.Mobile;
if (m?.Deleted == false && m.X == loc.m_X && m.Y == loc.m_Y)
{
_current = current;
@ -125,7 +141,7 @@ public partial class Map
{
current = current.Next;
m = current.Mobile;
m = current?.Mobile;
if (m?.Deleted == false && m.X == loc.m_X && m.Y == loc.m_Y)
{
_current = current;
@ -163,10 +179,10 @@ public partial class Map
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileEnumerator GetEnumerator() => new(_map, _bounds, _makeBoundsInclusive);
public ClientBoundsEnumerator GetEnumerator() => new(_map, _bounds, _makeBoundsInclusive);
}
public ref struct MobileEnumerator
public ref struct ClientBoundsEnumerator
{
private readonly Map _map;
private readonly int _sectorStartX;
@ -182,7 +198,7 @@ public partial class Map
private NetState _current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileEnumerator(Map map, Rectangle2D bounds, bool makeBoundsInclusive)
public ClientBoundsEnumerator(Map map, Rectangle2D bounds, bool makeBoundsInclusive)
{
_map = map;
_bounds = bounds;
@ -220,7 +236,6 @@ public partial class Map
throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion);
}
Mobile m;
NetState current = _current;
ref Rectangle2D bounds = ref _bounds;
var currentSectorX = _currentSectorX;
@ -255,7 +270,7 @@ public partial class Map
current = _linkList._first;
}
m = current.Mobile;
var m = current.Mobile;
if (m?.Deleted == false && bounds.Contains(m.Location))
{
_current = current;

View file

@ -0,0 +1,325 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Map.ItemByDistanceEnumerator.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Runtime.CompilerServices;
using Server.Collections;
namespace Server;
public partial class Map
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point3D p) =>
GetItemsInRangeByDistance<Item>(p, Core.GlobalMaxUpdateRange);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point3D p, int range) =>
GetItemsInRangeByDistance<Item>(p, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point3D p) where T : Item =>
GetItemsInRangeByDistance<T>(p, Core.GlobalMaxUpdateRange);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point3D p, int range) where T : Item =>
GetItemsInRangeByDistance<T>(p.m_X, p.m_Y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point2D p) =>
GetItemsInRangeByDistance<Item>(p, Core.GlobalMaxUpdateRange);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point2D p, int range) =>
GetItemsInRangeByDistance<Item>(p, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point2D p) where T : Item =>
GetItemsInRangeByDistance<T>(p, Core.GlobalMaxUpdateRange);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point2D p, int range) where T : Item =>
GetItemsInRangeByDistance<T>(p.m_X, p.m_Y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(int x, int y, int range) =>
GetItemsInRangeByDistance<Item>(x, y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(int x, int y, int range) where T : Item
{
var clampedRange = Math.Max(0, range);
var edge = clampedRange * 2 + 1;
return GetItemsInBoundsByDistance<T>(
new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge),
new Point2D(x, y)
);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<Item> GetItemsInBoundsByDistance(Rectangle2D bounds, bool makeBoundsInclusive = false) =>
GetItemsInBoundsByDistance<Item>(bounds, makeBoundsInclusive);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable<T> GetItemsInBoundsByDistance<T>(Rectangle2D bounds, bool makeBoundsInclusive = false)
where T : Item =>
GetItemsInBoundsByDistance<T>(
bounds,
new Point2D(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2),
makeBoundsInclusive
);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ItemDistanceEnumerable<T> GetItemsInBoundsByDistance<T>(
Rectangle2D bounds, Point2D center, bool makeBoundsInclusive = false
) where T : Item => new(this, bounds, center, makeBoundsInclusive);
public ref struct ItemDistanceEnumerable<T> where T : Item
{
private readonly Map _map;
private readonly Rectangle2D _bounds;
private readonly Point2D _center;
private readonly bool _makeBoundsInclusive;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerable(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive)
{
_map = map;
_bounds = bounds;
_center = center;
_makeBoundsInclusive = makeBoundsInclusive;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerator<T> GetEnumerator() => new(_map, _bounds, _center, _makeBoundsInclusive);
}
public ref struct ItemDistanceEnumerator<T> where T : Item
{
private Map _map;
private Point2D _center;
private Rectangle2D _bounds;
private int _sectorStartX;
private int _maxRing;
private int _ring; // -1 = uninitialized, then 0.._maxRing
private int _ringIndex; // Current index within the ring
private int _currentSectorX;
private int _currentSectorY;
private ref readonly ValueLinkList<Item> _linkList;
private int _currentVersion;
private T _current;
private int _minDistance;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemDistanceEnumerator(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive)
{
_map = map;
_center = center;
_bounds = makeBoundsInclusive
? new Rectangle2D(bounds.X, bounds.Y, bounds.Width + 1, bounds.Height + 1)
: bounds;
_current = null;
if (map != null)
{
var centerSectorX = center.m_X / SectorSize;
var centerSectorY = center.m_Y / SectorSize;
map.CalculateSectors(_bounds, out _sectorStartX, out var sectorStartY, out var sectorEndX, out var sectorEndY);
// Calculate max ring based on bounds
var dx = Math.Max(centerSectorX - _sectorStartX, sectorEndX - centerSectorX);
var dy = Math.Max(centerSectorY - sectorStartY, sectorEndY - centerSectorY);
_maxRing = Math.Max(dx, dy);
}
_ring = -1;
_ringIndex = -1;
_currentSectorX = 0;
_currentSectorY = 0;
_currentVersion = 0;
_minDistance = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
var map = _map;
if (map == null)
{
return false;
}
if (!Unsafe.IsNullRef(in _linkList) && _linkList.Version != _currentVersion)
{
throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion);
}
Item current = _current;
while (true)
{
current = current?.Next;
while (current == null)
{
while (!TryNextSectorInRing(out _currentSectorX, out _currentSectorY))
{
// Current ring exhausted, try next ring
if (_ring >= _maxRing)
{
return false; // No more rings to search
}
_ring++;
_ringIndex = -1;
}
_linkList = ref map.GetRealSector(_currentSectorX, _currentSectorY).Items;
_currentVersion = _linkList.Version;
current = _linkList._first;
if (current != null)
{
_minDistance = MinDistToSectorSqrt(_center.m_X, _center.m_Y, _currentSectorX, _currentSectorY);
}
}
if (current is T { Deleted: false } o && _bounds.Contains(o.Location))
{
_current = o;
return true;
}
}
}
public (T Value, int MinDistance) Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (_current, _minDistance);
}
private bool TryNextSectorInRing(out int sx, out int sy)
{
if (_ring == 0)
{
// Center sector
if (_ringIndex < 0)
{
_ringIndex = 0;
sx = _center.m_X / SectorSize;
sy = _center.m_Y / SectorSize;
return sx >= _sectorStartX;
}
sx = sy = 0;
return false;
}
var totalSectors = _ring * 8;
// Keep trying sectors in this ring until we find a valid one or exhaust the ring
while (true)
{
var nextIndex = _ringIndex + 1;
if (nextIndex >= totalSectors)
{
sx = sy = 0;
return false;
}
_ringIndex = nextIndex;
CalculatePositionFromIndex(nextIndex, out sx, out sy);
if (sx >= _sectorStartX)
{
return true;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CalculatePositionFromIndex(int index, out int x, out int y)
{
var centerSectorX = _center.m_X / SectorSize;
var centerSectorY = _center.m_Y / SectorSize;
var ringSize = _ring * 2;
var startX = centerSectorX - _ring;
var startY = centerSectorY - _ring;
if (index <= ringSize) // Top edge
{
x = startX + index;
y = startY;
}
else if (index <= ringSize * 2) // Right edge
{
x = startX + ringSize;
y = startY + (index - ringSize);
}
else if (index <= ringSize * 3) // Bottom edge
{
x = startX + ringSize - (index - ringSize * 2);
y = startY + ringSize;
}
else // Left edge
{
x = startX;
y = startY + ringSize - (index - ringSize * 3);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int MinDistToSectorSqrt(int cx, int cy, int sectorX, int sectorY)
{
var x0 = sectorX * SectorSize;
var y0 = sectorY * SectorSize;
var x1 = x0 + (SectorSize - 1);
var y1 = y0 + (SectorSize - 1);
var dx = 0;
if (cx < x0)
{
dx = x0 - cx;
}
else if (cx > x1)
{
dx = cx - x1;
}
var dy = 0;
if (cy < y0)
{
dy = y0 - cy;
}
else if (cy > y1)
{
dy = cy - y1;
}
return (int)Math.Sqrt(dx * dx + dy * dy);
}
}
}

View file

@ -69,8 +69,12 @@ public partial class Map
GetItemsInRange<T>(p.m_X, p.m_Y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemBoundsEnumerable<T> GetItemsInRange<T>(int x, int y, int range) where T : Item =>
GetItemsInBounds<T>(new Rectangle2D(x - range, y - range, range * 2 + 1, range * 2 + 1));
public ItemBoundsEnumerable<T> GetItemsInRange<T>(int x, int y, int range) where T : Item
{
var clampedRange = Math.Max(0, range);
var edge = clampedRange * 2 + 1;
return GetItemsInBounds<T>(new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ItemBoundsEnumerable<Item> GetItemsInBounds(Rectangle2D bounds) => GetItemsInBounds<Item>(bounds);

View file

@ -0,0 +1,320 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Map.MobileByDistanceEnumerator.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Runtime.CompilerServices;
using Server.Collections;
namespace Server;
public partial class Map
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point3D p) =>
GetMobilesInRangeByDistance<Mobile>(p, Core.GlobalMaxUpdateRange);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point3D p, int range) =>
GetMobilesInRangeByDistance<Mobile>(p, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point3D p) where T : Mobile =>
GetMobilesInRangeByDistance<T>(p, Core.GlobalMaxUpdateRange);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point3D p, int range) where T : Mobile =>
GetMobilesInRangeByDistance<T>(p.m_X, p.m_Y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point2D p) =>
GetMobilesInRangeByDistance<Mobile>(p, Core.GlobalMaxUpdateRange);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point2D p, int range) =>
GetMobilesInRangeByDistance<Mobile>(p, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point2D p) where T : Mobile =>
GetMobilesInRangeByDistance<T>(p, Core.GlobalMaxUpdateRange);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point2D p, int range) where T : Mobile =>
GetMobilesInRangeByDistance<T>(p.m_X, p.m_Y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(int x, int y, int range) =>
GetMobilesInRangeByDistance<Mobile>(x, y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(int x, int y, int range) where T : Mobile
{
var clampedRange = Math.Max(0, range);
var edge = clampedRange * 2 + 1;
return GetMobilesInBoundsByDistance<T>(
new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge),
new Point2D(x, y)
);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<Mobile> GetMobilesInBoundsByDistance(Rectangle2D bounds, bool makeBoundsInclusive = false) =>
GetMobilesInBoundsByDistance<Mobile>(bounds, makeBoundsInclusive);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable<T> GetMobilesInBoundsByDistance<T>(Rectangle2D bounds, bool makeBoundsInclusive = false) where T : Mobile =>
GetMobilesInBoundsByDistance<T>(bounds, new Point2D(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2), makeBoundsInclusive);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private MobileDistanceEnumerable<T> GetMobilesInBoundsByDistance<T>(
Rectangle2D bounds, Point2D center, bool makeBoundsInclusive = false
) where T : Mobile => new(this, bounds, center, makeBoundsInclusive);
public ref struct MobileDistanceEnumerable<T> where T : Mobile
{
private readonly Map _map;
private readonly Rectangle2D _bounds;
private readonly Point2D _center;
private readonly bool _makeBoundsInclusive;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerable(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive)
{
_map = map;
_bounds = bounds;
_center = center;
_makeBoundsInclusive = makeBoundsInclusive;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerator<T> GetEnumerator() => new(_map, _bounds, _center, _makeBoundsInclusive);
}
public ref struct MobileDistanceEnumerator<T> where T : Mobile
{
private Map _map;
private Point2D _center;
private Rectangle2D _bounds;
private int _sectorStartX;
private int _maxRing;
private int _ring; // -1 = uninitialized, then 0.._maxRing
private int _ringIndex; // Current index within the ring
private int _currentSectorX;
private int _currentSectorY;
private ref readonly ValueLinkList<Mobile> _linkList;
private int _currentVersion;
private T _current;
private int _minDistance;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileDistanceEnumerator(Map map, Rectangle2D bounds, Point2D center, bool makeBoundsInclusive)
{
_map = map;
_center = center;
_bounds = makeBoundsInclusive
? new Rectangle2D(bounds.X, bounds.Y, bounds.Width + 1, bounds.Height + 1)
: bounds;
_current = null;
if (map != null)
{
var centerSectorX = center.m_X / SectorSize;
var centerSectorY = center.m_Y / SectorSize;
map.CalculateSectors(_bounds, out _sectorStartX, out var sectorStartY, out var sectorEndX, out var sectorEndY);
// Calculate max ring based on bounds
var dx = Math.Max(centerSectorX - _sectorStartX, sectorEndX - centerSectorX);
var dy = Math.Max(centerSectorY - sectorStartY, sectorEndY - centerSectorY);
_maxRing = Math.Max(dx, dy);
}
_ring = -1;
_ringIndex = -1;
_currentSectorX = 0;
_currentSectorY = 0;
_currentVersion = 0;
_minDistance = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
var map = _map;
if (map == null)
{
return false;
}
if (!Unsafe.IsNullRef(in _linkList) && _linkList.Version != _currentVersion)
{
throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion);
}
Mobile current = _current;
while (true)
{
current = current?.Next;
while (current == null)
{
while (!TryNextSectorInRing(out _currentSectorX, out _currentSectorY))
{
// Current ring exhausted, try next ring
if (_ring >= _maxRing)
{
return false; // No more rings to search
}
_ring++;
_ringIndex = -1;
}
_linkList = ref map.GetRealSector(_currentSectorX, _currentSectorY).Mobiles;
_currentVersion = _linkList.Version;
current = _linkList._first;
if (current != null)
{
_minDistance = MinDistToSectorSqrt(_center.m_X, _center.m_Y, _currentSectorX, _currentSectorY);
}
}
if (current is T { Deleted: false } o && _bounds.Contains(o.Location))
{
_current = o;
return true;
}
}
}
public (T Value, int MinDistance) Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (_current, _minDistance);
}
private bool TryNextSectorInRing(out int sx, out int sy)
{
if (_ring == 0)
{
// Center sector
if (_ringIndex < 0)
{
_ringIndex = 0;
sx = _center.m_X / SectorSize;
sy = _center.m_Y / SectorSize;
return sx >= _sectorStartX;
}
sx = sy = 0;
return false;
}
var totalSectors = _ring * 8;
// Keep trying sectors in this ring until we find a valid one or exhaust the ring
while (true)
{
var nextIndex = _ringIndex + 1;
if (nextIndex >= totalSectors)
{
sx = sy = 0;
return false;
}
_ringIndex = nextIndex;
CalculatePositionFromIndex(nextIndex, out sx, out sy);
if (sx >= _sectorStartX)
{
return true;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CalculatePositionFromIndex(int index, out int x, out int y)
{
var centerSectorX = _center.m_X / SectorSize;
var centerSectorY = _center.m_Y / SectorSize;
var ringSize = _ring * 2;
var startX = centerSectorX - _ring;
var startY = centerSectorY - _ring;
if (index <= ringSize) // Top edge
{
x = startX + index;
y = startY;
}
else if (index <= ringSize * 2) // Right edge
{
x = startX + ringSize;
y = startY + (index - ringSize);
}
else if (index <= ringSize * 3) // Bottom edge
{
x = startX + ringSize - (index - ringSize * 2);
y = startY + ringSize;
}
else // Left edge
{
x = startX;
y = startY + ringSize - (index - ringSize * 3);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int MinDistToSectorSqrt(int cx, int cy, int sectorX, int sectorY)
{
var x0 = sectorX * SectorSize;
var y0 = sectorY * SectorSize;
var x1 = x0 + (SectorSize - 1);
var y1 = y0 + (SectorSize - 1);
var dx = 0;
if (cx < x0)
{
dx = x0 - cx;
}
else if (cx > x1)
{
dx = cx - x1;
}
var dy = 0;
if (cy < y0)
{
dy = y0 - cy;
}
else if (cy > y1)
{
dy = cy - y1;
}
return (int)Math.Sqrt(dx * dx + dy * dy);
}
}
}

View file

@ -69,8 +69,12 @@ public partial class Map
GetMobilesInRange<T>(p.m_X, p.m_Y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileBoundsEnumerable<T> GetMobilesInRange<T>(int x, int y, int range) where T : Mobile =>
GetMobilesInBounds<T>(new Rectangle2D(x - range, y - range, range * 2 + 1, range * 2 + 1));
public MobileBoundsEnumerable<T> GetMobilesInRange<T>(int x, int y, int range) where T : Mobile
{
var clampedRange = Math.Max(0, range);
var edge = clampedRange * 2 + 1;
return GetMobilesInBounds<T>(new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MobileBoundsEnumerable<Mobile> GetMobilesInBounds(Rectangle2D bounds) => GetMobilesInBounds<Mobile>(bounds);

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Map.MultiEnumerator.cs *
* *
@ -17,15 +17,13 @@ using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Server.Collections;
using Server.Items;
namespace Server;
public partial class Map
{
private static SectorMultiValueLinkList _emptyMultiLinkList = new();
public static ref readonly SectorMultiValueLinkList EmptyMultiLinkList => ref _emptyMultiLinkList;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MultiSectorEnumerable<BaseMulti> GetMultisInSector(Point3D p) => GetMultisInSector<BaseMulti>(p);
@ -72,8 +70,12 @@ public partial class Map
GetMultisInRange<T>(p.m_X, p.m_Y, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MultiBoundsEnumerable<T> GetMultisInRange<T>(int x, int y, int range) where T : BaseMulti =>
GetMultisInBounds<T>(new Rectangle2D(x - range, y - range, range * 2 + 1, range * 2 + 1));
public MultiBoundsEnumerable<T> GetMultisInRange<T>(int x, int y, int range) where T : BaseMulti
{
var clampedRange = Math.Max(0, range);
var edge = clampedRange * 2 + 1;
return GetMultisInBounds<T>(new Rectangle2D(x - clampedRange, y - clampedRange, edge, edge));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MultiBoundsEnumerable<BaseMulti> GetMultisInBounds(Rectangle2D bounds, bool makeBoundsInclusive = false) =>
@ -83,6 +85,8 @@ public partial class Map
public MultiBoundsEnumerable<T> GetMultisInBounds<T>(Rectangle2D bounds, bool makeBoundsInclusive = false) where T : BaseMulti =>
new(this, bounds, makeBoundsInclusive);
private static readonly HashSet<Serial> _sharedDupes = [];
public ref struct MultiSectorEnumerable<T>(Map map, Point2D loc) where T : BaseMulti
{
public static MultiSectorEnumerable<T> Empty
@ -98,27 +102,42 @@ public partial class Map
public ref struct MultiSectorEnumerator<T> where T : BaseMulti
{
private readonly Span<BaseMulti> _list;
private readonly int _version;
private readonly Sector _sector;
private int _index;
private T _current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MultiSectorEnumerator(Map map, Point2D loc)
{
_list = map == null
? Span<BaseMulti>.Empty
: CollectionsMarshal.AsSpan(map.GetSector(loc.m_X, loc.m_Y).Multis);
if (map == null)
{
_list = Span<BaseMulti>.Empty;
_sector = null;
_version = 0;
}
else
{
_sector = map.GetSector(loc.m_X, loc.m_Y);
_list = CollectionsMarshal.AsSpan(_sector.Multis);
_version = _sector.MultisVersion;
}
_index = 0;
_index = -1;
_current = null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
while ((uint)_index < (uint)_list.Length)
if (_sector != null && _version != _sector.MultisVersion)
{
var current = _list[_index++];
if (current is T { Deleted: false } o)
throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion);
}
while (++_index < _list.Length)
{
if (_list[_index] is T { Deleted: false } o)
{
_current = o;
return true;
@ -160,24 +179,27 @@ public partial class Map
public ref struct MultiBoundsEnumerator<T> where T : BaseMulti
{
private readonly Map _map;
private readonly int _sectorStartX;
private readonly int _sectorEndX;
private readonly int _sectorEndY;
private Map _map;
private int _sectorStartX;
private int _sectorEndX;
private int _sectorEndY;
private Rectangle2D _bounds;
private int _currentSectorX;
private int _currentSectorY;
private Span<BaseMulti> _list;
private Span<BaseMulti> _currentList;
private int _currentIndex;
private int _currentVersion;
private Sector _currentSector;
private T _current;
private int _index;
private HashSet<Serial> _dupes;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MultiBoundsEnumerator(Map map, Rectangle2D bounds, bool makeBoundsInclusive)
{
_sharedDupes.Clear();
_map = map;
_bounds = bounds;
@ -196,62 +218,78 @@ public partial class Map
// We start the X sector one short because it gets incremented immediately in MoveNext()
_currentSectorX = _sectorStartX - 1;
_currentSectorY = _sectorStartY;
_index = 0;
}
_currentList = default;
_currentIndex = -1;
_currentVersion = 0;
_currentSector = null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool GetMulti()
public bool MoveNext()
{
ref Rectangle2D bounds = ref _bounds;
var map = _map;
while ((uint)_index < (uint)_list.Length)
if (map == null)
{
var current = _list[_index++];
_dupes ??= new HashSet<Serial>();
if (current is T { Deleted: false } o && bounds.Contains(o.Location) && !_dupes.Contains(o.Serial))
{
_dupes.Add(o.Serial);
_current = o;
return true;
}
return false;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool GetSector()
{
ref Rectangle2D bounds = ref _bounds;
var currentSectorX = _currentSectorX;
var currentSectorY = _currentSectorY;
var sectorEndX = _sectorEndX;
var sectorEndY = _sectorEndY;
// Move to next sector
if (currentSectorX < sectorEndX)
while (true)
{
_currentSectorX = ++currentSectorX;
}
else if (currentSectorY < sectorEndY)
{
_currentSectorX = currentSectorX = _sectorStartX;
_currentSectorY = ++currentSectorY;
}
else
{
// Ran out of sectors
return false;
}
// Try to advance in the current list
if (_currentList.Length > 0)
{
if (_currentVersion != _currentSector.MultisVersion)
{
throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion);
}
_list = CollectionsMarshal.AsSpan(_map.GetRealSector(currentSectorX, currentSectorY).Multis);
return GetMulti();
while (++_currentIndex < _currentList.Length)
{
var item = _currentList[_currentIndex];
if (item is T { Deleted: false } o && bounds.Contains(o.Location))
{
// Multis can span multiple sectors, so we need to deduplicate
if (_sharedDupes.Add(o.Serial))
{
_current = o;
return true;
}
}
}
}
// Move to next sector
if (currentSectorX < sectorEndX)
{
_currentSectorX = ++currentSectorX;
}
else if (currentSectorY < sectorEndY)
{
_currentSectorX = currentSectorX = _sectorStartX;
_currentSectorY = ++currentSectorY;
}
else
{
// Ran out of sectors
return false;
}
_currentSector = map.GetRealSector(currentSectorX, currentSectorY);
_currentList = CollectionsMarshal.AsSpan(_currentSector.Multis);
_currentVersion = _currentSector.MultisVersion;
_currentIndex = -1;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() => _map != null && (GetMulti() || GetSector());
public T Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Map.cs *
* *
@ -1355,11 +1355,13 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
{
// TODO: Can we avoid this?
private static readonly List<Region> m_DefaultRectList = new();
private static readonly List<BaseMulti> m_DefaultMultiList = new();
private bool m_Active;
private ValueLinkList<NetState> _clients;
private ValueLinkList<Item> _items;
private ValueLinkList<Mobile> _mobiles;
private List<BaseMulti> _multis = new();
private List<BaseMulti> _multis;
private int _multisVersion;
private List<Region> _regions;
public Sector(int x, int y, Map owner)
@ -1372,9 +1374,11 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
public List<Region> Regions => _regions ?? m_DefaultRectList;
internal List<BaseMulti> Multis => _multis;
internal List<BaseMulti> Multis => _multis ?? m_DefaultMultiList;
internal ref ValueLinkList<Mobile> Mobiles => ref _mobiles;
internal int MultisVersion => _multisVersion;
internal ref readonly ValueLinkList<Mobile> Mobiles => ref _mobiles;
internal ref readonly ValueLinkList<Item> Items => ref _items;
@ -1503,12 +1507,17 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
public void OnMultiEnter(BaseMulti multi)
{
_multis ??= new List<BaseMulti>();
_multis.Add(multi);
_multisVersion++;
}
public void OnMultiLeave(BaseMulti multi)
{
_multis.Remove(multi);
if (_multis?.Remove(multi) == true)
{
_multisVersion++;
}
}
public void Activate()

View file

@ -0,0 +1,63 @@
using System.Runtime.CompilerServices;
namespace Server;
public partial class Mobile
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemAtEnumerable<Item> GetItemsAt() =>
m_Map == null ? Map.ItemAtEnumerable<Item>.Empty : m_Map.GetItemsAt(m_Location);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemAtEnumerable<T> GetItemsAt<T>() where T : Item =>
m_Map == null ? Map.ItemAtEnumerable<T>.Empty : m_Map.GetItemsAt<T>(m_Location);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemBoundsEnumerable<Item> GetItemsInRange(int range) => GetItemsInRange<Item>(range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemBoundsEnumerable<T> GetItemsInRange<T>(int range) where T : Item =>
m_Map == null ? Map.ItemBoundsEnumerable<T>.Empty : m_Map.GetItemsInRange<T>(m_Location, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileAtEnumerable<Mobile> GetMobilesInRange() => GetMobilesInRange<Mobile>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileAtEnumerable<T> GetMobilesInRange<T>() where T : Mobile =>
m_Map == null ? Map.MobileAtEnumerable<T>.Empty : m_Map.GetMobilesAt<T>(m_Location);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileBoundsEnumerable<Mobile> GetMobilesInRange(int range) => GetMobilesInRange<Mobile>(range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileBoundsEnumerable<T> GetMobilesInRange<T>(int range) where T : Mobile =>
m_Map == null ? Map.MobileBoundsEnumerable<T>.Empty : m_Map.GetMobilesInRange<T>(m_Location, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ClientAtEnumerable GetClientsAt() =>
m_Map == null ? Map.ClientAtEnumerable.Empty : Map.GetClientsAt(m_Location);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ClientBoundsEnumerable GetClientsInRange(int range) =>
m_Map == null ? Map.ClientBoundsEnumerable.Empty : Map.GetClientsInRange(m_Location, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(int range) =>
GetItemsInRangeByDistance<Item>(range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(int range) where T : Item =>
m_Map == null ? default : m_Map.GetItemsInRangeByDistance<T>(m_Location, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(int range) =>
GetMobilesInRangeByDistance<Mobile>(range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(int range) where T : Mobile =>
m_Map == null ? default : m_Map.GetMobilesInRangeByDistance<T>(m_Location, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ClientDistanceEnumerable GetClientsInRangeByDistance(int range) =>
m_Map == null ? default : m_Map.GetClientsInRangeByDistance(m_Location, range);
}

View file

@ -2737,8 +2737,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
const int cacheLength = OutgoingMobilePackets.MobileMovingPacketCacheByteLength;
Span<byte> mobileMovingCache = stackalloc byte[cacheLength];
mobileMovingCache.Clear();
Span<byte> mobileMovingCache = stackalloc byte[cacheLength].InitializePacket();
var ourState = m_NetState;
@ -2887,6 +2886,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
Span<byte> deadBuffer = stackalloc byte[OutgoingMobilePackets.BondedStatusPacketLength].InitializePacket();
Span<byte> removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
Span<byte> hitsPacket = stackalloc byte[OutgoingMobilePackets.MobileAttributePacketLength].InitializePacket();
mobileMovingCache.InitializePacket();
foreach (var state in Map.GetClientsInRange(m_Location))
{
@ -3723,38 +3723,6 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
{
}
public double GetDistanceToSqrt(Point3D p)
{
var xDelta = m_Location.m_X - p.m_X;
var yDelta = m_Location.m_Y - p.m_Y;
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
}
public double GetDistanceToSqrt(Mobile m)
{
var xDelta = m_Location.m_X - m.m_Location.m_X;
var yDelta = m_Location.m_Y - m.m_Location.m_Y;
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
}
public double GetDistanceToSqrt(Point2D p)
{
var xDelta = m_Location.m_X - p.X;
var yDelta = m_Location.m_Y - p.Y;
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
}
public double GetDistanceToSqrt(IPoint2D p)
{
var xDelta = m_Location.m_X - p.X;
var yDelta = m_Location.m_Y - p.Y;
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
}
public virtual void AggressiveAction(Mobile aggressor) => AggressiveAction(aggressor, false);
public virtual void AggressiveAction(Mobile aggressor, bool criminal)
@ -4360,7 +4328,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
const int cacheLength = OutgoingMobilePackets.MobileMovingPacketCacheByteLength;
Span<byte> mobileMovingCache = stackalloc byte[cacheLength];
mobileMovingCache.Clear();
mobileMovingCache.InitializePacket();
while (moveClientQueue.Count > 0)
{
@ -6995,12 +6963,9 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
flags |= 0x04;
}
}
else
else if (m_Poison != null)
{
if (m_Poison != null)
{
flags |= 0x04;
}
flags |= 0x04;
}
if (m_Blessed || m_YellowHealthbar)
@ -8067,43 +8032,6 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
return -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemAtEnumerable<Item> GetItemsAt() =>
m_Map == null ? Map.ItemAtEnumerable<Item>.Empty : m_Map.GetItemsAt(m_Location);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemAtEnumerable<T> GetItemsAt<T>() where T : Item =>
m_Map == null ? Map.ItemAtEnumerable<T>.Empty : m_Map.GetItemsAt<T>(m_Location);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemBoundsEnumerable<Item> GetItemsInRange(int range) => GetItemsInRange<Item>(range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemBoundsEnumerable<T> GetItemsInRange<T>(int range) where T : Item =>
m_Map == null ? Map.ItemBoundsEnumerable<T>.Empty : m_Map.GetItemsInRange<T>(m_Location, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileAtEnumerable<Mobile> GetMobilesInRange() => GetMobilesInRange<Mobile>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileAtEnumerable<T> GetMobilesInRange<T>() where T : Mobile =>
m_Map == null ? Map.MobileAtEnumerable<T>.Empty : m_Map.GetMobilesAt<T>(m_Location);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileBoundsEnumerable<Mobile> GetMobilesInRange(int range) => GetMobilesInRange<Mobile>(range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileBoundsEnumerable<T> GetMobilesInRange<T>(int range) where T : Mobile =>
m_Map == null ? Map.MobileBoundsEnumerable<T>.Empty : m_Map.GetMobilesInRange<T>(m_Location, range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ClientAtEnumerable GetClientsAt() =>
m_Map == null ? Map.ClientAtEnumerable.Empty : Map.GetClientsAt(m_Location);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ClientBoundsEnumerable GetClientsInRange(int range) =>
m_Map == null ? Map.ClientBoundsEnumerable.Empty : Map.GetClientsInRange(m_Location, range);
public void SayTo(Mobile to, bool ascii, string text) =>
PrivateOverheadMessage(MessageType.Regular, SpeechHue, ascii, text, to.NetState);
@ -8318,7 +8246,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
Region.OnDidHarmful(this, target);
target.Region.OnGotHarmful(this, target);
if (!indirect)
if (!indirect && !ChangingCombatant)
{
Combatant = target;
}

View file

@ -25,8 +25,10 @@ public static class OutgoingMobilePackets
public const int BondedStatusPacketLength = 11;
public const int DeathAnimationPacketLength = 13;
public const int MobileMovingPacketLength = 17;
public const int MobileMovingPacketCacheHeight = 7 * 2; // 7 notoriety, 2 client versions
public const int MobileMovingPacketCacheByteLength = MobileMovingPacketLength * MobileMovingPacketCacheHeight;
// Mobile Moving Packet plus 2 bytes for regular/stygian flags
public const int MobileMovingPacketCacheByteLength = MobileMovingPacketLength + 2;
public const int AttributeMaximum = 100;
public const int MobileAttributePacketLength = 9;
public const int MobileAttributesPacketLength = 17;
@ -99,27 +101,26 @@ public static class OutgoingMobilePackets
ns.Send(span);
}
public static void CreateMobileMoving(Span<byte> buffer, Mobile m, int noto, bool stygianAbyss)
public static void CreateMobileMoving(Span<byte> buffer, Mobile m, int noto, byte packetFlags)
{
if (buffer[0] != 0)
if (buffer[0] == 0)
{
return;
var loc = m.Location;
var hue = m.SolidHueOverride >= 0 ? m.SolidHueOverride : m.Hue;
var writer = new SpanWriter(buffer);
writer.Write((byte)0x77); // Packet ID
writer.Write(m.Serial);
writer.Write((short)m.Body);
writer.Write((short)loc.m_X);
writer.Write((short)loc.m_Y);
writer.Write((sbyte)loc.m_Z);
writer.Write((byte)m.Direction);
writer.Write((short)hue);
}
var loc = m.Location;
var hue = m.SolidHueOverride >= 0 ? m.SolidHueOverride : m.Hue;
var writer = new SpanWriter(buffer);
writer.Write((byte)0x77); // Packet ID
writer.Write(m.Serial);
writer.Write((short)m.Body);
writer.Write((short)loc.m_X);
writer.Write((short)loc.m_Y);
writer.Write((sbyte)loc.m_Z);
writer.Write((byte)m.Direction);
writer.Write((short)hue);
writer.Write((byte)m.GetPacketFlags(stygianAbyss));
writer.Write((byte)noto);
buffer[15] = packetFlags;
buffer[16] = (byte)noto;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -134,7 +135,8 @@ public static class OutgoingMobilePackets
}
Span<byte> buffer = stackalloc byte[MobileMovingPacketLength].InitializePacket();
CreateMobileMoving(buffer, target, noto, ns.StygianAbyss);
var packetFlags = (byte)target.GetPacketFlags(ns.StygianAbyss);
CreateMobileMoving(buffer, target, noto, packetFlags);
ns.Send(buffer);
}
@ -142,8 +144,6 @@ public static class OutgoingMobilePackets
public static void SendMobileMovingUsingCache(this NetState ns, Span<byte> cache, Mobile source, Mobile target) =>
ns.SendMobileMovingUsingCache(cache, target, Notoriety.Compute(source, target));
// Requires a buffer of 14 packets, 17 bytes per packet (238 bytes).
// Requires cache to have the first byte of each packet initially zeroed.
public static void SendMobileMovingUsingCache(this NetState ns, Span<byte> cache, Mobile target, int noto)
{
if (ns.CannotSendPackets())
@ -151,13 +151,15 @@ public static class OutgoingMobilePackets
return;
}
var stygianAbyss = ns.StygianAbyss;
// Indexes 0-6 for pre-SA, and 7-13 for SA
var row = noto + (stygianAbyss ? 6 : -1);
var buffer = cache.Slice(row * MobileMovingPacketLength, MobileMovingPacketLength);
CreateMobileMoving(buffer, target, noto, stygianAbyss);
// Cache the packet flags for regular/stygian if the packet hasn't been built yet
if (cache[0] == 0)
{
cache[17] = (byte)target.GetPacketFlags(false);
cache[18] = (byte)target.GetPacketFlags(true);
}
ns.Send(buffer);
CreateMobileMoving(cache, target, noto, ns.StygianAbyss ? cache[18] : cache[17]);
ns.Send(cache[..17]);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View file

@ -547,10 +547,29 @@ public class Region : IComparable<Region>, IValueLinkListNode<Region>
public virtual bool AcceptsSpawnsFrom(Region region) =>
AllowSpawn() && (region == this || Parent?.AcceptsSpawnsFrom(region) == true);
public PooledRefList<Mobile> GetPlayersPooled()
{
var list = PooledRefList<Mobile>.Create();
for (var i = 0; i < Sectors?.Length; i++)
{
var sector = Sectors[i];
foreach (var ns in sector.Clients)
{
var player = ns.Mobile;
if (player?.Deleted == false && player.Region.IsPartOf(this))
{
list.Add(ns.Mobile);
}
}
}
return list;
}
public List<Mobile> GetPlayers()
{
var list = new List<Mobile>();
List<Mobile> list = [];
for (var i = 0; i < Sectors?.Length; i++)
{
var sector = Sectors[i];
@ -609,6 +628,25 @@ public class Region : IComparable<Region>, IValueLinkListNode<Region>
return list;
}
public PooledRefList<Mobile> GetMobilesPooled()
{
var list = PooledRefList<Mobile>.Create();
for (var i = 0; i < Sectors?.Length; i++)
{
var sector = Sectors[i];
foreach (var mobile in sector.Mobiles)
{
if (mobile.Region.IsPartOf(this))
{
list.Add(mobile);
}
}
}
return list;
}
public int GetMobileCount()
{
var count = 0;
@ -649,6 +687,26 @@ public class Region : IComparable<Region>, IValueLinkListNode<Region>
return list;
}
public PooledRefList<Item> GetItemsPooled()
{
var list = PooledRefList<Item>.Create();
for (var i = 0; i < Sectors?.Length; i++)
{
var sector = Sectors[i];
foreach (var item in sector.Items)
{
if (Find(item.Location, item.Map).IsPartOf(this))
{
list.Add(item);
}
}
}
return list;
}
public int GetItemCount()
{
var count = 0;

View file

@ -37,10 +37,10 @@
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
<PackageReference Include="LibDeflate.Bindings" Version="1.0.2.120" />
<PackageReference Include="PollGroup" Version="1.6.1" />
<PackageReference Include="System.IO.Hashing" Version="9.0.4" />
<PackageReference Include="System.IO.Hashing" Version="10.0.0" />
<PackageReference Include="ModernUO.Serialization.Annotations" Version="2.9.1" />
<PackageReference Include="ModernUO.Serialization.Generator" Version="2.12.20" />
<PackageReference Include="ModernUO.Serialization.Generator" Version="2.13.0" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Migrations/*.v*.json" />

View file

@ -14,236 +14,270 @@
*************************************************************************/
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Text;
using Server.Buffers;
using Server.Text;
namespace Server;
public enum TextAlignment : byte
{
Left = 0,
Center = 1,
Right = 2
}
public static class Html
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Color(
scoped ref RawInterpolatedStringHandler textHandler,
ReadOnlySpan<char> color,
int size = -1, byte fontStyle = 0
)
public static string Center(this string input) => Center(input.AsSpan());
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(this ReadOnlySpan<char> input) => $"<CENTER>{input}</CENTER>";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(ref RawInterpolatedStringHandler input)
{
var handler = textHandler.Text.Color(color, size, fontStyle);
textHandler.Clear();
return handler;
var str = input.Text.Center();
input.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Color(
this ReadOnlySpan<char> text,
ReadOnlySpan<char> color,
int size = -1,
byte fontStyle = 0
public static string Center(this string input, int color) => Center(input.AsSpan(), color);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(this string input, ReadOnlySpan<char> color) => Center(input.AsSpan(), color);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(this ReadOnlySpan<char> input, int color) =>
$"<CENTER><BASEFONT COLOR=#{color:X6}>{input}</BASEFONT></CENTER>";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(this ReadOnlySpan<char> input, ReadOnlySpan<char> color) =>
$"<CENTER><BASEFONT COLOR={color}>{input}</BASEFONT></CENTER>";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(ref RawInterpolatedStringHandler input, int color)
{
var str = input.Text.Center(color);
input.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(ref RawInterpolatedStringHandler input, ReadOnlySpan<char> color)
{
var str = input.Text.Center(color);
input.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(this string input, int color) => Color(input.AsSpan(), color);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(this string input, ReadOnlySpan<char> color) => Color(input.AsSpan(), color);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(this ReadOnlySpan<char> input, int color) => $"<BASEFONT COLOR=#{color:X6}>{input}</CENTER>";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(this ReadOnlySpan<char> input, ReadOnlySpan<char> color) =>
$"<BASEFONT COLOR={color}>{input}</CENTER>";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(ref RawInterpolatedStringHandler input, int color)
{
var str = input.Text.Color(color);
input.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(ref RawInterpolatedStringHandler input, ReadOnlySpan<char> color)
{
var str = input.Text.Color(color);
input.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this string input) => Right(input.AsSpan());
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this ReadOnlySpan<char> input) => $"<RIGHT>{input}</RIGHT>";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(ref RawInterpolatedStringHandler input)
{
var str = input.Text.Right();
input.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this string input, int color) => Right(input.AsSpan(), color);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this string input, ReadOnlySpan<char> color) => Right(input.AsSpan(), color);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this ReadOnlySpan<char> input, int color) =>
$"<RIGHT><BASEFONT COLOR=#{color:X6}>{input}</BASEFONT></RIGHT>";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this ReadOnlySpan<char> input, ReadOnlySpan<char> color) =>
$"<RIGHT><BASEFONT COLOR={color}>{input}</BASEFONT></RIGHT>";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(ref RawInterpolatedStringHandler input, int color)
{
var str = input.Text.Right(color);
input.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(ref RawInterpolatedStringHandler input, ReadOnlySpan<char> color)
{
var str = input.Text.Right(color);
input.Clear();
return str;
}
private static readonly SearchValues<char> _htmlSearchValues = SearchValues.Create('<', '>', '&', '"', '\'');
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string EscapeHtml(this string input)
{
if (string.IsNullOrEmpty(input))
{
return input ?? "";
}
return EscapeHtml(input.AsSpan());
}
public static string EscapeHtml(this ReadOnlySpan<char> input)
{
if (input.IsEmpty)
{
return string.Empty;
}
int indexOfAny = input.IndexOfAny(_htmlSearchValues);
if (indexOfAny < 0)
{
return input.ToString();
}
using var builder = ValueStringBuilder.Create(input.Length * 2);
int lastIndex = 0;
while (indexOfAny >= 0)
{
if (indexOfAny > lastIndex)
{
builder.Append(input[lastIndex..indexOfAny]);
}
char c = input[indexOfAny];
var replacement = c switch
{
'&' => "&amp;",
'<' => "&lt;",
'>' => "&gt;",
'"' => "&quot;",
'\'' => "&#39;"
};
builder.Append(replacement);
lastIndex = indexOfAny + 1;
indexOfAny = input[lastIndex..].IndexOfAny(_htmlSearchValues);
if (indexOfAny < 0)
{
break;
}
indexOfAny += lastIndex;
}
if (lastIndex < input.Length)
{
builder.Append(input[lastIndex..]);
}
var result = builder.ToString();
builder.Dispose();
return result;
}
public static string Build(
ReadOnlySpan<char> text, ReadOnlySpan<char> color = default, int size = -1, byte fontStyle = 0,
TextAlignment align = TextAlignment.Left
)
{
if (color != Span<char>.Empty)
var arr = STArrayPool<char>.Shared.Rent(BuildCharCount(text, color));
var bytesWritten = Build(text, arr.AsSpan(), color, size, fontStyle, align);
var result = arr.AsSpan(0, bytesWritten).ToString();
STArrayPool<char>.Shared.Return(arr);
return result;
}
public static int BuildCharCount(ReadOnlySpan<char> text, ReadOnlySpan<char> color) => 61 + text.Length + color.Length;
public static int Build(
ReadOnlySpan<char> text, Span<char> dest, ReadOnlySpan<char> color = default, int size = -1, byte fontStyle = 0,
TextAlignment align = TextAlignment.Left
)
{
using var builder = new ValueStringBuilder(dest);
if (align == TextAlignment.Right)
{
builder.Append("<RIGHT>");
}
else if (align == TextAlignment.Center)
{
builder.Append("<CENTER>");
}
if (color.Length > 0 || size > -1 || fontStyle > 0)
{
builder.Append("<BASEFONT");
if (color.Length > 0)
{
builder.Append($" COLOR={color}");
}
if (size > -1)
{
if (fontStyle > 0)
{
return $"<BASEFONT COLOR={color} SIZE={size} STYLE={fontStyle}>{text}</BASEFONT>";
}
return $"<BASEFONT COLOR={color} SIZE={size}>{text}</BASEFONT>";
builder.Append($" SIZE={size}");
}
if (fontStyle > 0)
{
return $"<BASEFONT COLOR={color} STYLE={fontStyle}>{text}</BASEFONT>";
builder.Append($" STYLE={fontStyle}");
}
return $"<BASEFONT COLOR={color}>{text}</BASEFONT>";
builder.Append($">{text}</BASEFONT>");
}
if (size > -1)
else
{
if (fontStyle > 0)
{
return $"<BASEFONT SIZE={size} STYLE={fontStyle}>{text}</BASEFONT>";
}
return $"<BASEFONT SIZE={size}>{text}</BASEFONT>";
builder.Append(text);
}
if (fontStyle > 0)
if (align == TextAlignment.Right)
{
return $"<BASEFONT STYLE={fontStyle}>{text}</BASEFONT>";
builder.Append("</RIGHT>");
}
return $"{text}";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Color(
this ReadOnlySpan<char> text,
int color,
int size = -1,
byte fontStyle = 0
)
{
if (color > -1)
else if (align == TextAlignment.Center)
{
return text.Color($"#{color:X6}", size, fontStyle);
builder.Append("</CENTER>");
}
return text.Color((ReadOnlySpan<char>)default, size, fontStyle);
return builder.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Color(
scoped ref RawInterpolatedStringHandler textHandler,
int color,
int size = -1,
byte fontStyle = 0
)
{
var handler = textHandler.Text.Color(color, size, fontStyle);
textHandler.Clear();
return handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(
this string text,
ReadOnlySpan<char> color,
int size = -1,
byte fontStyle = 0
)
{
var textHandler = ((ReadOnlySpan<char>)text).Color(color, size, fontStyle);
var str = textHandler.Text.ToString();
textHandler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(
this string text,
int color,
int size = -1,
byte fontStyle = 0
)
{
var textHandler = ((ReadOnlySpan<char>)text).Color(color, size, fontStyle);
var str = textHandler.Text.ToString();
textHandler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(
this string text, int color, int size = -1, byte fontStyle = 0
)
{
var handler = Center((ReadOnlySpan<char>)text, color, size, fontStyle);
var str = handler.Text.ToString();
handler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(
this string text, ReadOnlySpan<char> color, int size = -1, byte fontStyle = 0
)
{
var handler = Center((ReadOnlySpan<char>)text, color, size, fontStyle);
var str = handler.Text.ToString();
handler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(this string text) => text.Center(-1);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Center(this ReadOnlySpan<char> text) => Center(text, -1);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Center(
this ReadOnlySpan<char> text, int color, int size = -1, byte fontStyle = 0
) => Color($"<CENTER>{text}</CENTER>", color, size, fontStyle);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Center(
this ReadOnlySpan<char> text, ReadOnlySpan<char> color, int size = -1, byte fontStyle = 0
) => Color($"<CENTER>{text}</CENTER>", color, size, fontStyle);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Center(
scoped ref RawInterpolatedStringHandler textHandler, int color = -1, int size = -1, byte fontStyle = 0
)
{
var handler = textHandler.Text.Center(color, size, fontStyle);
textHandler.Clear();
return handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(
this string text, int color, int size = -1, byte fontStyle = 0
)
{
var handler = Right((ReadOnlySpan<char>)text, color, size, fontStyle);
var str = handler.Text.ToString();
handler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(
this string text, ReadOnlySpan<char> color, int size = -1, byte fontStyle = 0
)
{
var handler = Right((ReadOnlySpan<char>)text, color, size, fontStyle);
var str = handler.Text.ToString();
handler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this string text) => text.Right(-1);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Right(
scoped ref RawInterpolatedStringHandler textHandler, int color = -1, int size = -1, byte fontStyle = 0
)
{
var handler = textHandler.Text.Right(color, size, fontStyle);
textHandler.Clear();
return handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Right(
this ReadOnlySpan<char> text, int color, int size = -1, byte fontStyle = 0
) => Color($"<RIGHT>{text}</RIGHT>", color, size, fontStyle);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Right(
this ReadOnlySpan<char> text, ReadOnlySpan<char> color, int size = -1, byte fontStyle = 0
) => Color($"<RIGHT>{text}</RIGHT>", color, size, fontStyle);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Right(this ReadOnlySpan<char> text) => text.Right(-1);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string EscapeHtml(this string input) =>
new StringBuilder(input.Length).Append(input)
.Replace("<", "&lt;")
.Replace(">", "&gt;")
.Replace("&", "&amp;")
.Replace("\"", "&quot;")
.Replace("'", "&#39;")
.ToString();
}

View file

@ -1479,4 +1479,98 @@ public static partial class Utility
return DateTime.SpecifyKind(local - tz.GetUtcOffset(local), DateTimeKind.Utc);
}
public static string FormatTimeCompact(this TimeSpan ts, bool showSeconds = false)
{
using var sb = ValueStringBuilder.Create();
if (ts.Days >= 1)
{
sb.Append($"{ts.Days}d");
}
if (sb.Length > 0)
{
sb.Append($" {ts.Hours}h");
}
else if (ts.Hours >= 1)
{
sb.Append($"{ts.Hours}h");
}
if (sb.Length > 0)
{
sb.Append($" {ts.Minutes}m");
}
else if (ts.Minutes >= 1)
{
sb.Append($"{ts.Minutes}m");
}
if (showSeconds)
{
if (sb.Length > 0)
{
sb.Append($" {ts.Seconds}s");
}
else if (ts.Seconds >= 1)
{
sb.Append($"{ts.Seconds}s");
}
}
else if (sb.Length == 0)
{
sb.Append("0m");
}
return sb.ToString();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double GetDistanceToSqrt(this IEntity entity, Point2D p) => GetDistanceToSqrt(entity.Location, p);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double GetDistanceToSqrt(this IEntity entity, Point3D p) => GetDistanceToSqrt(entity.Location, p);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double GetDistanceToSqrt(this IEntity from, IEntity to) => GetDistanceToSqrt(from.Location, to.Location);
public static double GetDistanceToSqrt(this IEntity entity, IPoint2D p)
{
var xDelta = entity.X - p.X;
var yDelta = entity.Y - p.Y;
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
}
public static double GetDistanceToSqrt(this Point2D from, Point2D to)
{
var xDelta = from.m_X - to.m_X;
var yDelta = from.m_Y - to.m_Y;
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
}
public static double GetDistanceToSqrt(this Point2D from, Point3D to)
{
var xDelta = from.m_X - to.m_X;
var yDelta = from.m_Y - to.m_Y;
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
}
public static double GetDistanceToSqrt(this Point3D from, Point2D to)
{
var xDelta = from.m_X - to.m_X;
var yDelta = from.m_Y - to.m_Y;
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
}
public static double GetDistanceToSqrt(this Point3D from, Point3D to)
{
var xDelta = from.m_X - to.m_X;
var yDelta = from.m_Y - to.m_Y;
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
}
}

View file

@ -27,7 +27,7 @@ public class DynamicTestGump : DynamicGump
builder.AddItem(218, 95, 0xCB0);
builder.AddHtml(30, 30, 150, 75, "<div align=center>Wilt thou sanctify the resurrection of:</div>");
builder.AddHtml(30, 70, 150, 25, $"<CENTER>{_petName}</CENTER>", true);
builder.AddHtml(30, 70, 150, 25, _petName, align: TextAlignment.Center, background: true);
builder.AddButton(40, 105, 0x81A, 0x81B, 0x1); // Okay
builder.AddButton(110, 105, 0x819, 0x818, 0x2); // Cancel

View file

@ -35,6 +35,6 @@ public class StaticLayoutTestGump : StaticGump<StaticLayoutTestGump>
protected override void BuildStrings(ref GumpStringsBuilder builder)
{
builder.SetHtmlTextCentered("petName", _petName);
builder.SetHtmlText("petName", _petName, align: TextAlignment.Center);
}
}

View file

@ -24,7 +24,7 @@ public class StaticTestGump : StaticGump<StaticTestGump>
builder.AddItem(218, 95, 0xCB0);
builder.AddHtml(30, 30, 150, 75, "<div align=center>Wilt thou sanctify the resurrection of:</div>");
builder.AddHtml(30, 70, 150, 25, "<CENTER>Test</CENTER>", true);
builder.AddHtml(30, 70, 150, 25, "Test", align: TextAlignment.Center, background: true);
builder.AddButton(40, 105, 0x81A, 0x81B, 0x1); // Okay
builder.AddButton(110, 105, 0x819, 0x818, 0x2); // Cancel

View file

@ -0,0 +1,299 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Server;
using Server.Mobiles;
using Server.SkillHandlers;
using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class TrackingTests
{
/// <summary>
/// Tests that tracking correctly finds the closest mobiles when there are more than 12 available.
/// This validates the GetClosestMobs logic, especially the early exit optimization.
/// </summary>
[Fact]
public void Tracking_FindsClosestMobiles_WhenManyAvailable()
{
var map = Map.Felucca;
var center = new Point3D(1000, 1000, 0);
var tracker = CreatePlayerMobile(map, center);
tracker.Skills.Tracking.BaseFixedPoint = 1000; // 100.0 skill = 110 range
var mobiles = new List<TestAnimal>();
try
{
// Create 20 animals at various distances
// First 12 should be the closest ones we find
for (var i = 0; i < 20; i++)
{
var distance = i + 1; // Distance from 1 to 20
var location = new Point3D(center.X + distance, center.Y, 0);
var animal = CreateAnimal(map, location);
mobiles.Add(animal);
}
// Invoke tracking through the skill system
// We can't directly test GetClosestMobs since it's private, but we can verify
// the behavior by checking what the gump would show
// Use reflection to test the private method
var method = typeof(TrackWhoGump).GetMethod(
"GetClosestMobs",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static
);
Assert.NotNull(method);
var range = Math.Clamp(10 + (int)tracker.Skills.Tracking.Value, 0, 100);
var result = (Mobile[])method.Invoke(null, new object[] { tracker, range, 0 }); // 0 = animals
// Should return at most 12 mobiles
Assert.True(result.Length <= 12);
// Should return the 12 closest (distances 1-12)
Assert.Equal(12, result.Length);
// Verify they are sorted by distance
for (var i = 0; i < result.Length - 1; i++)
{
var dist1 = result[i].GetDistanceToSqrt(center);
var dist2 = result[i + 1].GetDistanceToSqrt(center);
Assert.True(dist1 <= dist2, $"Mobiles not sorted: {dist1} > {dist2}");
}
// Verify the first mobile is the closest (distance 1)
Assert.Equal(mobiles[0], result[0]);
// Verify the last mobile is at distance 12
Assert.Equal(mobiles[11], result[11]);
// Verify mobile at distance 13 is NOT included
Assert.DoesNotContain(mobiles[12], result);
}
finally
{
tracker?.Delete();
foreach (var mob in mobiles)
{
mob?.Delete();
}
}
}
/// <summary>
/// Tests that tracking correctly handles the case where there are exactly 12 mobiles available.
/// </summary>
[Fact]
public void Tracking_FindsAllMobiles_WhenExactly12Available()
{
var map = Map.Felucca;
var center = new Point3D(2000, 2000, 0);
var tracker = CreatePlayerMobile(map, center);
tracker.Skills.Tracking.BaseFixedPoint = 1000; // 100.0 skill = 110 range
var mobiles = new List<TestAnimal>();
try
{
// Create exactly 12 animals
for (var i = 0; i < 12; i++)
{
var distance = i + 1;
var location = new Point3D(center.X + distance, center.Y, 0);
var animal = CreateAnimal(map, location);
mobiles.Add(animal);
}
var method = typeof(TrackWhoGump).GetMethod(
"GetClosestMobs",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static
);
var range = Math.Clamp(10 + (int)tracker.Skills.Tracking.Value, 0, 100);
var result = (Mobile[])method.Invoke(null, new object[] { tracker, range, 0 });
Assert.Equal(12, result.Length);
// Verify all mobiles are included
foreach (var mob in mobiles)
{
Assert.Contains(mob, result);
}
}
finally
{
tracker?.Delete();
foreach (var mob in mobiles)
{
mob?.Delete();
}
}
}
/// <summary>
/// Tests that tracking correctly handles the case where there are fewer than 12 mobiles available.
/// </summary>
[Fact]
public void Tracking_FindsAllMobiles_WhenFewerThan12Available()
{
var map = Map.Felucca;
var center = new Point3D(3000, 3000, 0);
var tracker = CreatePlayerMobile(map, center);
tracker.Skills.Tracking.BaseFixedPoint = 1000;
var mobiles = new List<TestAnimal>();
try
{
// Create only 5 animals
for (var i = 0; i < 5; i++)
{
var distance = i + 1;
var location = new Point3D(center.X + distance, center.Y, 0);
var animal = CreateAnimal(map, location);
mobiles.Add(animal);
}
var method = typeof(TrackWhoGump).GetMethod(
"GetClosestMobs",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static
);
var range = Math.Clamp(10 + (int)tracker.Skills.Tracking.Value, 0, 100);
var result = (Mobile[])method.Invoke(null, new object[] { tracker, range, 0 });
Assert.Equal(5, result.Length);
// Verify all mobiles are included
foreach (var mob in mobiles)
{
Assert.Contains(mob, result);
}
}
finally
{
tracker?.Delete();
foreach (var mob in mobiles)
{
mob?.Delete();
}
}
}
/// <summary>
/// Tests that the early exit optimization works correctly when mobiles in farther sectors
/// are closer than mobiles in nearer sectors (worst case for the optimization).
/// </summary>
[Fact]
public void Tracking_EarlyExitWorksCorrectly_WithFarSectorNearMobiles()
{
var map = Map.Felucca;
// Use a location that puts mobiles in different sectors
var center = new Point3D(1500, 1500, 0);
var tracker = CreatePlayerMobile(map, center);
tracker.Skills.Tracking.BaseFixedPoint = 1000;
var mobiles = new List<TestAnimal>();
try
{
// Create 15 animals where some farther ones might be in closer proximity
// but in different sectors
for (var i = 0; i < 15; i++)
{
var distance = i + 1;
// Alternate between X and Y to potentially cross sector boundaries
var location = i % 2 == 0
? new Point3D(center.X + distance, center.Y, 0)
: new Point3D(center.X, center.Y + distance, 0);
var animal = CreateAnimal(map, location);
mobiles.Add(animal);
}
var method = typeof(TrackWhoGump).GetMethod(
"GetClosestMobs",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static
);
var range = Math.Clamp(10 + (int)tracker.Skills.Tracking.Value, 0, 100);
var result = (Mobile[])method.Invoke(null, new object[] { tracker, range, 0 });
Assert.True(result.Length <= 12);
// Get the actual 12 closest by brute force
var allDistances = mobiles
.Select(m => (Mobile: m, Distance: m.GetDistanceToSqrt(center)))
.OrderBy(x => x.Distance)
.ToList();
var expected12Closest = allDistances.Take(12).ToList();
// The result should match the actual 12 closest
Assert.Equal(expected12Closest.Count, result.Length);
// Verify all returned mobiles are in the expected 12 closest
foreach (var mob in result)
{
Assert.Contains(mob, expected12Closest.Select(x => x.Mobile));
}
// Verify they are sorted by distance (main invariant)
for (var i = 0; i < result.Length - 1; i++)
{
var dist1 = result[i].GetDistanceToSqrt(center);
var dist2 = result[i + 1].GetDistanceToSqrt(center);
Assert.True(dist1 <= dist2, $"Mobiles not sorted by distance: {dist1} > {dist2}");
}
// Verify we got the closest mobiles (not just any 12)
var maxResultDistance = result.Max(m => m.GetDistanceToSqrt(center));
var minExcludedDistance = allDistances.Skip(12).Any()
? allDistances.Skip(12).Min(x => x.Distance)
: double.MaxValue;
Assert.True(maxResultDistance <= minExcludedDistance,
$"Found a closer excluded mobile: max in result={maxResultDistance}, min excluded={minExcludedDistance}");
}
finally
{
tracker?.Delete();
foreach (var mob in mobiles)
{
mob?.Delete();
}
}
}
private static PlayerMobile CreatePlayerMobile(Map map, Point3D location)
{
var mobile = new PlayerMobile(World.NewMobile);
mobile.DefaultMobileInit();
mobile.MoveToWorld(location, map);
return mobile;
}
private static TestAnimal CreateAnimal(Map map, Point3D location)
{
var animal = new TestAnimal(World.NewMobile);
animal.DefaultMobileInit();
animal.MoveToWorld(location, map);
return animal;
}
private class TestAnimal : BaseCreature
{
public TestAnimal(Serial serial) : base(serial)
{
Body = 0xD8; // Llama body - an animal body
}
}
}

View file

@ -4,9 +4,9 @@
<Configurations>Debug;Release;Analyze</Configurations>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -850,40 +850,6 @@ public partial class Account : IAccount, IComparable<Account>
return true;
}
/// <summary>
/// Deserializes a list of string values from an xml element. Null values are not added to the list.
/// </summary>
/// <param name="node">The XmlElement from which to deserialize.</param>
/// <returns>String list. Value will never be null.</returns>
private static string[] LoadAccessCheck(XmlElement node)
{
string[] stringList;
var accessCheck = node["accessCheck"];
if (accessCheck != null)
{
var list = new List<string>();
foreach (XmlElement ip in accessCheck.GetElementsByTagName("ip"))
{
var text = Utility.GetText(ip, null);
if (text != null)
{
list.Add(text);
}
}
stringList = list.ToArray();
}
else
{
stringList = [];
}
return stringList;
}
/// <summary>
/// Deserializes a list of IPAddress values from an xml element.
/// </summary>

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: AccountSecurity.cs *
* *
@ -15,56 +15,55 @@
using System;
namespace Server.Accounting.Security
{
public enum PasswordProtectionAlgorithm
{
// Obsolete algorithms from RunUO. These are not secure!
// They are included for password upgrades only.
None,
MD5,
SHA1,
namespace Server.Accounting.Security;
// Supported algorithms
SHA2, // ServUO compatibility
PBKDF2,
Argon2 // Recommended algorithm for real security.
public enum PasswordProtectionAlgorithm
{
// Obsolete algorithms from RunUO. These are not secure!
// They are included for password upgrades only.
None,
MD5,
SHA1,
// Supported algorithms
SHA2, // ServUO compatibility
PBKDF2,
Argon2 // Recommended algorithm for real security.
}
public static class AccountSecurity
{
public static PasswordProtectionAlgorithm CurrentAlgorithm { get; set; }
public static IPasswordProtection CurrentPasswordProtection => GetPasswordProtection(CurrentAlgorithm);
public static void Configure()
{
CurrentAlgorithm =
ServerConfiguration.GetOrUpdateSetting(
"accountSecurity.encryptionAlgorithm",
PasswordProtectionAlgorithm.Argon2
);
if (CurrentAlgorithm < PasswordProtectionAlgorithm.SHA2)
{
throw new Exception($"Security: {CurrentAlgorithm} is obsolete and not secure. Do not use it.");
}
}
public static class AccountSecurity
public static IPasswordProtection GetPasswordProtection(PasswordProtectionAlgorithm algorithm)
{
public static PasswordProtectionAlgorithm CurrentAlgorithm { get; set; }
public static IPasswordProtection CurrentPasswordProtection => GetPasswordProtection(CurrentAlgorithm);
public static void Configure()
var passwordProtection = algorithm switch
{
CurrentAlgorithm =
ServerConfiguration.GetOrUpdateSetting(
"accountSecurity.encryptionAlgorithm",
PasswordProtectionAlgorithm.Argon2
);
PasswordProtectionAlgorithm.MD5 => HashAlgorithmPasswordProtection.MD5Instance,
PasswordProtectionAlgorithm.SHA1 => HashAlgorithmPasswordProtection.SHA1Instance,
PasswordProtectionAlgorithm.SHA2 => HashAlgorithmPasswordProtection.SHA2Instance,
PasswordProtectionAlgorithm.PBKDF2 => PBKDF2PasswordProtection.Instance,
PasswordProtectionAlgorithm.Argon2 => Argon2PasswordProtection.Instance,
PasswordProtectionAlgorithm.None => throw new Exception("Do not use PasswordProtectionAlgorithm.None"),
_ => throw new Exception("No algorithm")
};
if (CurrentAlgorithm < PasswordProtectionAlgorithm.SHA2)
{
throw new Exception($"Security: {CurrentAlgorithm} is obsolete and not secure. Do not use it.");
}
}
public static IPasswordProtection GetPasswordProtection(PasswordProtectionAlgorithm algorithm)
{
var passwordProtection = algorithm switch
{
PasswordProtectionAlgorithm.MD5 => HashAlgorithmPasswordProtection.MD5Instance,
PasswordProtectionAlgorithm.SHA1 => HashAlgorithmPasswordProtection.SHA1Instance,
PasswordProtectionAlgorithm.SHA2 => HashAlgorithmPasswordProtection.SHA2Instance,
PasswordProtectionAlgorithm.PBKDF2 => PBKDF2PasswordProtection.Instance,
PasswordProtectionAlgorithm.Argon2 => Argon2PasswordProtection.Instance,
PasswordProtectionAlgorithm.None => throw new Exception("Do not use PasswordProtectionAlgorithm.None"),
_ => throw new Exception("No algorithm")
};
return passwordProtection;
}
return passwordProtection;
}
}

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Argon2PasswordProtection.cs *
* *
@ -15,18 +15,17 @@
using System.Security.Cryptography;
namespace Server.Accounting.Security
namespace Server.Accounting.Security;
public class Argon2PasswordProtection : IPasswordProtection
{
public class Argon2PasswordProtection : IPasswordProtection
{
public static IPasswordProtection Instance = new Argon2PasswordProtection();
public static IPasswordProtection Instance = new Argon2PasswordProtection();
private readonly Argon2PasswordHasher m_PasswordHasher = new(rng: BuiltInSecureRng.Generator);
private readonly Argon2PasswordHasher m_PasswordHasher = new(rng: RandomNumberGenerator.Create());
public string EncryptPassword(string plainPassword) =>
m_PasswordHasher.Hash(plainPassword);
public string EncryptPassword(string plainPassword) =>
m_PasswordHasher.Hash(plainPassword);
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
m_PasswordHasher.Verify(encryptedPassword, plainPassword);
}
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
m_PasswordHasher.Verify(encryptedPassword, plainPassword);
}

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: HashAlgorithmPasswordProtection.cs *
* *
@ -17,24 +17,23 @@ using System;
using System.Security.Cryptography;
using Server.Text;
namespace Server.Accounting.Security
namespace Server.Accounting.Security;
public class HashAlgorithmPasswordProtection : IPasswordProtection
{
public class HashAlgorithmPasswordProtection : IPasswordProtection
public static IPasswordProtection MD5Instance = new HashAlgorithmPasswordProtection(MD5.Create());
public static IPasswordProtection SHA1Instance = new HashAlgorithmPasswordProtection(SHA1.Create());
public static IPasswordProtection SHA2Instance = new HashAlgorithmPasswordProtection(SHA512.Create());
private readonly HashAlgorithm _hashAlgorithm;
public HashAlgorithmPasswordProtection(HashAlgorithm hashAlgorithm) => _hashAlgorithm = hashAlgorithm;
public string EncryptPassword(string plainPassword)
{
public static IPasswordProtection MD5Instance = new HashAlgorithmPasswordProtection(MD5.Create());
public static IPasswordProtection SHA1Instance = new HashAlgorithmPasswordProtection(SHA1.Create());
public static IPasswordProtection SHA2Instance = new HashAlgorithmPasswordProtection(SHA512.Create());
private readonly HashAlgorithm _hashAlgorithm;
public HashAlgorithmPasswordProtection(HashAlgorithm hashAlgorithm) => _hashAlgorithm = hashAlgorithm;
public string EncryptPassword(string plainPassword)
{
byte[] bytes = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length)).GetBytesAscii();
return _hashAlgorithm.ComputeHash(bytes).ToHexString();
}
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
EncryptPassword(plainPassword) == encryptedPassword;
byte[] bytes = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length)).GetBytesAscii();
return _hashAlgorithm.ComputeHash(bytes).ToHexString();
}
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
EncryptPassword(plainPassword) == encryptedPassword;
}

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: PBKDF2PasswordProtection.cs *
* *
@ -18,42 +18,43 @@ using System.Buffers.Binary;
using System.Security.Cryptography;
using Server.Text;
namespace Server.Accounting.Security
namespace Server.Accounting.Security;
public class PBKDF2PasswordProtection : IPasswordProtection
{
public class PBKDF2PasswordProtection : IPasswordProtection
private const ushort m_MinIterations = 1024;
private const ushort m_MaxIterations = 1536;
private const int m_SaltSize = 8;
private const int m_HashSize = 32;
private const int m_OutputSize = 2 + m_SaltSize + m_HashSize;
public static readonly IPasswordProtection Instance = new PBKDF2PasswordProtection();
public string EncryptPassword(string plainPassword)
{
private const ushort m_MinIterations = 1024;
private const ushort m_MaxIterations = 1536;
private const int m_SaltSize = 8;
private const int m_HashSize = 32;
private const int m_OutputSize = 2 + m_SaltSize + m_HashSize;
public static readonly IPasswordProtection Instance = new PBKDF2PasswordProtection();
Span<byte> output = stackalloc byte[m_OutputSize];
var iterations = Utility.RandomMinMax(m_MinIterations, m_MaxIterations);
BinaryPrimitives.WriteUInt16LittleEndian(output[..2], (ushort)iterations);
public string EncryptPassword(string plainPassword)
{
Span<byte> output = stackalloc byte[m_OutputSize];
var iterations = Utility.RandomMinMax(m_MinIterations, m_MaxIterations);
BinaryPrimitives.WriteUInt16LittleEndian(output[..2], (ushort)iterations);
var salt = output.Slice(2, m_SaltSize);
RandomNumberGenerator.Fill(salt);
var rfc2898 = new Rfc2898DeriveBytes(plainPassword, m_SaltSize, iterations, HashAlgorithmName.SHA256);
rfc2898.Salt.CopyTo(output.Slice(2, m_SaltSize));
rfc2898.GetBytes(m_HashSize).CopyTo(output[(m_SaltSize + 2)..]);
var hash = output.Slice(2 + m_SaltSize, m_HashSize);
Rfc2898DeriveBytes.Pbkdf2(plainPassword, salt, hash, iterations, HashAlgorithmName.SHA256);
return output.ToHexString();
}
return output.ToHexString();
}
public bool ValidatePassword(string encryptedPassword, string plainPassword)
{
Span<byte> encryptedBytes = stackalloc byte[m_OutputSize];
encryptedPassword.GetBytes(encryptedBytes);
public bool ValidatePassword(string encryptedPassword, string plainPassword)
{
Span<byte> encryptedBytes = stackalloc byte[m_OutputSize];
encryptedPassword.GetBytes(encryptedBytes);
var iterations = BinaryPrimitives.ReadUInt16LittleEndian(encryptedBytes[..2]);
var salt = encryptedBytes.Slice(2, m_SaltSize);
var iterations = BinaryPrimitives.ReadUInt16LittleEndian(encryptedBytes[..2]);
var salt = encryptedBytes.Slice(2, m_SaltSize);
ReadOnlySpan<byte> hash =
new Rfc2898DeriveBytes(plainPassword, salt.ToArray(), iterations, HashAlgorithmName.SHA256).GetBytes(m_HashSize);
Span<byte> hash = stackalloc byte[m_HashSize];
Rfc2898DeriveBytes.Pbkdf2(plainPassword, salt, hash, iterations, HashAlgorithmName.SHA256);
return hash.SequenceEqual(encryptedBytes[(m_SaltSize + 2)..]);
}
return hash.SequenceEqual(encryptedBytes[(m_SaltSize + 2)..]);
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Server.Collections;
using Server.Gumps;
using Server.Items;
using Server.Multis;
@ -112,7 +113,7 @@ namespace Server.Commands.Generic
if (okay)
{
var foundations = new List<HouseFoundation>();
using var foundations = PooledRefQueue<HouseFoundation>.Create();
flushToLog = list.Count > 20;
for (var i = 0; i < list.Count; ++i)
@ -127,7 +128,7 @@ namespace Server.Commands.Generic
if (!foundations.Contains(house))
{
foundations.Add(house);
foundations.Enqueue(house);
}
break;
@ -146,9 +147,9 @@ namespace Server.Commands.Generic
}
}
foreach (var house in foundations)
while (foundations.Count > 0)
{
house.Delta(ItemDelta.Update);
foundations.Dequeue().Delta(ItemDelta.Update);
}
}
else

View file

@ -33,8 +33,10 @@ namespace Server.Commands.Generic
if (mobiles)
{
foreach (var mob in reg.GetMobiles())
using var mobileList = reg.GetMobilesPooled();
for (var i = 0; i < mobileList.Count; i++)
{
var mob = mobileList[i];
if (BaseCommand.IsAccessible(from, mob) && ext.IsValid(mob))
{
list.Add(mob);
@ -44,7 +46,8 @@ namespace Server.Commands.Generic
if (items)
{
foreach (var item in reg.GetItems())
using var itemList = reg.GetItemsPooled();
foreach (var item in itemList)
{
if (BaseCommand.IsAccessible(from, item) && ext.IsValid(item))
{

View file

@ -361,7 +361,7 @@ public static class HelpInfo
builder.AddPage();
builder.AddBackground(0, 0, _width, _height, 5054);
builder.AddHtml(10, 10, _width - 20, 20, _info.Name.Center(0xFF0000));
builder.AddHtml(10, 10, _width - 20, 20, _info.Name, color: "#FF0000", align: TextAlignment.Center);
using var sb = ValueStringBuilder.Create();
@ -412,7 +412,7 @@ public static class HelpInfo
}
sb.Append(_info.Description);
builder.AddHtml(10, 40, _width - 20, _height - 80, sb.ToString(), false, true);
builder.AddHtml(10, 40, _width - 20, _height - 80, sb.AsSpan(true), background: false, scrollbar: true);
}
}
}

View file

@ -14,11 +14,9 @@ namespace Server.Engines.BulkOrders
_material = material;
}
public BaseBOD() : base(Core.AOS ? 0x2258 : 0x14EF)
{
Weight = 1.0;
LootType = LootType.Blessed;
}
public BaseBOD() : base(Core.AOS ? 0x2258 : 0x14EF) => LootType = LootType.Blessed;
public override double DefaultWeight => 1.0;
public abstract bool Complete { get; }

File diff suppressed because it is too large Load diff

View file

@ -3,135 +3,142 @@ using Server.Items;
using Server.Mobiles;
using Server.Network;
namespace Server.Engines.BulkOrders
namespace Server.Engines.BulkOrders;
public class BODBuyGump : StaticGump<BODBuyGump>
{
public class BODBuyGump : Gump
private BOBGump _gump;
private readonly IBOBEntry _entry;
private readonly int _price;
public BODBuyGump(BOBGump gump, IBOBEntry entry, int price) : base(100, 200)
{
private readonly BulkOrderBook m_Book;
private readonly IBOBEntry m_Entry;
private readonly PlayerMobile m_From;
private readonly int m_Page;
private readonly int m_Price;
_gump = gump;
_entry = entry;
_price = price;
}
public BODBuyGump(PlayerMobile from, BulkOrderBook book, IBOBEntry entry, int page, int price) : base(100, 200)
protected override void BuildLayout(ref StaticGumpBuilder builder)
{
builder.AddPage();
builder.AddBackground(100, 10, 300, 150, 5054);
builder.AddHtmlLocalized(125, 20, 250, 24, 1019070); // You have agreed to purchase:
builder.AddHtmlLocalized(125, 45, 250, 24, 1045151); // a bulk order deed
builder.AddHtmlLocalized(125, 70, 250, 24, 1019071); // for the amount of:
builder.AddLabelPlaceholder(125, 95, 0, "price");
builder.AddButton(250, 130, 4005, 4007, 1);
builder.AddHtmlLocalized(282, 130, 100, 24, 1011012); // CANCEL
builder.AddButton(120, 130, 4005, 4007, 2);
builder.AddHtmlLocalized(152, 130, 100, 24, 1011036); // OKAY
}
protected override void BuildStrings(ref GumpStringsBuilder builder)
{
builder.SetStringSlot("price", $"{_price:N0}");
}
public override void OnResponse(NetState sender, in RelayInfo info)
{
if (sender.Mobile is not PlayerMobile pm)
{
m_From = from;
m_Book = book;
m_Entry = entry;
m_Price = price;
m_Page = page;
AddPage(0);
AddBackground(100, 10, 300, 150, 5054);
AddHtmlLocalized(125, 20, 250, 24, 1019070); // You have agreed to purchase:
AddHtmlLocalized(125, 45, 250, 24, 1045151); // a bulk order deed
AddHtmlLocalized(125, 70, 250, 24, 1019071); // for the amount of:
AddLabel(125, 95, 0, price.ToString());
AddButton(250, 130, 4005, 4007, 1);
AddHtmlLocalized(282, 130, 100, 24, 1011012); // CANCEL
AddButton(120, 130, 4005, 4007, 2);
AddHtmlLocalized(152, 130, 100, 24, 1011036); // OKAY
return;
}
public override void OnResponse(NetState sender, in RelayInfo info)
if (info.ButtonID != 2)
{
if (info.ButtonID != 2)
{
m_From.SendLocalizedMessage(503207); // Cancelled purchase.
return;
}
pm.SendLocalizedMessage(503207); // Cancelled purchase.
return;
}
if (m_Book.RootParent is not PlayerVendor pv)
{
m_From.SendLocalizedMessage(1062382); // The deed selected is not available.
return;
}
var book = _gump.Book;
if (!m_Book.Entries.Contains(m_Entry))
{
pv.SayTo(m_From, 1062382); // The deed selected is not available.
return;
}
if (book.RootParent is not PlayerVendor pv)
{
pm.SendLocalizedMessage(1062382); // The deed selected is not available.
return;
}
var price = 0;
if (!book.Entries.Contains(_entry))
{
pv.SayTo(pm, 1062382); // The deed selected is not available.
return;
}
if (pv.GetVendorItem(m_Book)?.IsForSale == false)
{
price = m_Entry.Price;
}
var price = 0;
if (price != m_Price)
{
pv.SayTo(
m_From,
"The price has been been changed. If you like, you may offer to purchase the item again."
);
return;
}
if (pv.GetVendorItem(book)?.IsForSale == false)
{
price = _entry.Price;
}
if (price == 0)
{
pv.SayTo(m_From, 1062382); // The deed selected is not available.
return;
}
if (price != _price)
{
pv.SayTo(
pm,
"The price has been been changed. If you like, you may offer to purchase the item again."
);
return;
}
var item = m_Entry.Reconstruct();
if (price == 0)
{
pv.SayTo(pm, 1062382); // The deed selected is not available.
return;
}
pv.Say(m_From.Name);
var item = _entry.Reconstruct();
var pack = m_From.Backpack;
pv.Say(pm.Name);
if (pack?.CheckHold(
m_From,
var pack = pm.Backpack;
if (pack?.CheckHold(
pm,
item,
true,
true,
0,
item.PileWeight + item.TotalWeight
) != true)
{
pv.SayTo(pm, 503204); // You do not have room in your backpack for this
pm.SendGump(_gump);
item.Delete();
}
else if (pack.ConsumeTotal(typeof(Gold), price) || Banker.Withdraw(pm, price))
{
book.RemoveEntry(_entry);
pv.HoldGold += price;
pm.AddToBackpack(item);
// The bulk order deed has been placed in your backpack.
pm.SendLocalizedMessage(1045152);
if (book.Entries.Count / 5 < book.ItemCount)
{
pv.SayTo(m_From, 503204); // You do not have room in your backpack for this
m_From.SendGump(new BOBGump(m_From, m_Book, m_Page));
item.Delete();
book.ItemCount--;
book.InvalidateItems();
}
if (book.Entries.Count > 0)
{
_gump.ResetList();
pm.SendGump(_gump);
}
else
{
if (pack.ConsumeTotal(typeof(Gold), price) || Banker.Withdraw(m_From, price))
{
m_Book.RemoveEntry(m_Entry);
m_Book.InvalidateProperties();
pv.HoldGold += price;
m_From.AddToBackpack(item);
// The bulk order deed has been placed in your backpack.
m_From.SendLocalizedMessage(1045152);
if (m_Book.Entries.Count / 5 < m_Book.ItemCount)
{
m_Book.ItemCount--;
m_Book.InvalidateItems();
}
if (m_Book.Entries.Count > 0)
{
m_From.SendGump(new BOBGump(m_From, m_Book, m_Page));
}
else
{
m_From.SendLocalizedMessage(1062381); // The book is empty.
}
}
else
{
pv.SayTo(m_From, 503205); // You cannot afford this item.
item.Delete();
}
pm.SendLocalizedMessage(1062381); // The book is empty.
}
}
else
{
pv.SayTo(pm, 503205); // You cannot afford this item.
item.Delete();
}
}
}

View file

@ -40,7 +40,6 @@ public partial class BulkOrderBook : Item, ISecurable
[Constructible]
public BulkOrderBook() : base(0x2259)
{
Weight = 1.0;
LootType = LootType.Blessed;
_entries = [];
@ -49,6 +48,8 @@ public partial class BulkOrderBook : Item, ISecurable
_level = SecureLevel.CoOwners;
}
public override double DefaultWeight => 1.0;
public override void OnAfterDuped(Item newItem)
{
if (newItem is not BulkOrderBook book)

View file

@ -1364,6 +1364,8 @@ public class ChampionSpawnRegion : BaseRegion
global = Math.Max(global, 1 + Spawn.Level); //This is a guesstimate. TODO: Verify & get exact values // OSI testing: at 2 red skulls, light = 0x3 ; 1 red = 0x3.; 3 = 8; 9 = 0xD 8 = 0xD 12 = 0x12 10 = 0xD
}
private static readonly HashSet<IPAddress> _addresses = [];
public override void OnEnter(Mobile m)
{
if (!m.Player || m.AccessLevel != AccessLevel.Player || Spawn.Active)
@ -1371,8 +1373,6 @@ public class ChampionSpawnRegion : BaseRegion
return;
}
Region parent = Parent ?? this;
if (Spawn.ReadyToActivate)
{
Spawn.Start();
@ -1384,27 +1384,29 @@ public class ChampionSpawnRegion : BaseRegion
return;
}
List<Mobile> players = parent.GetPlayers();
List<IPAddress> addresses = new List<IPAddress>();
using var players = (Parent ?? this).GetPlayersPooled();
for (var i = 0; i < players.Count; i++)
{
if (players[i].AccessLevel == AccessLevel.Player && players[i].NetState != null &&
!addresses.Contains(players[i].NetState.Address) && !((PlayerMobile)players[i]).Young)
var player = players[i];
if (player.AccessLevel == AccessLevel.Player && player.NetState != null && !((PlayerMobile)player).Young)
{
addresses.Add(players[i].NetState.Address);
_addresses.Add(player.NetState.Address);
}
}
if (addresses.Count >= 15)
if (_addresses.Count >= 15)
{
foreach (Mobile player in players)
for (var i = 0; i < players.Count; i++)
{
player.SendMessage(0x20, Spawn.BroadcastMessage);
players[i].SendMessage(0x20, Spawn.BroadcastMessage);
}
Spawn.ActivatedByProximity = true;
Spawn.BeginRestart(TimeSpan.FromMinutes(5.0));
}
_addresses.Clear();
}
public override bool OnMoveInto(Mobile m, Direction d, Point3D newLocation, Point3D oldLocation)

View file

@ -14,7 +14,7 @@
*************************************************************************/
using System;
using System.Collections.Generic;
using Server.Collections;
using Server.Logging;
namespace Server.Engines.CannedEvil;
@ -67,18 +67,18 @@ public static class ChampionGenerator
*/
//We assume that all champion spawns are generated here.
List<ChampionSpawn> spawns = [];
using var spawns = PooledRefQueue<IEntity>.Create();
foreach (Item item in World.Items.Values)
{
if (item is ChampionSpawn spawn)
{
spawns.Add(spawn);
spawns.Enqueue(spawn);
}
}
for (int i = spawns.Count - 1; i >= 0; i--)
while (spawns.Count > 0)
{
spawns[i].Delete();
spawns.Dequeue().Delete();
}
Process(DungeonLocations);

View file

@ -5,6 +5,7 @@ using System.Text;
using Server.Gumps;
using Server.Mobiles;
using Server.Network;
using Server.Text;
namespace Server.Engines.ConPVP
{
@ -82,7 +83,7 @@ namespace Server.Engines.ConPVP
AddImage(215, -43, 0xEE40);
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(128);
if (tourney.TourneyType == TourneyType.FreeForAll)
{
@ -117,12 +118,13 @@ namespace Server.Engines.ConPVP
if (tourney.EventController != null)
{
sb.Append(' ').Append(tourney.EventController.Title);
sb.Append(' ');
sb.Append(tourney.EventController.Title);
}
sb.Append(" Tournament Invitation");
AddBorderedText(22, 22, 294, 20, sb.ToString().Center(), LabelColor32, BlackColor32);
AddBorderedText(22, 22, 294, 20, sb.AsSpan().Center(), LabelColor32, BlackColor32);
AddBorderedText(
22,

View file

@ -189,7 +189,7 @@ namespace Server.Engines.ConPVP
AddBorderedText(x + 5, y + 5, 325 - 5, sb.ToString(), color, 0);
x += 325;
AddBorderedText(x, y + 5, 40, ar.Spectators.ToString().Center(), color, 0);
AddBorderedText(x, y + 5, 40, Html.Center($"{ar.Spectators}"), color, 0);
}
}

View file

@ -7,6 +7,7 @@ using Server.Gumps;
using Server.Mobiles;
using Server.Network;
using Server.Targeting;
using Server.Text;
namespace Server.Engines.ConPVP
{
@ -92,7 +93,7 @@ namespace Server.Engines.ConPVP
AddImage(215, -43, 0xEE40);
// AddImage( 330, 141, 0x8BA );
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(128);
if (tourney.TourneyType == TourneyType.FreeForAll)
{
@ -127,12 +128,13 @@ namespace Server.Engines.ConPVP
if (tourney.EventController != null)
{
sb.Append(' ').Append(tourney.EventController.Title);
sb.Append(' ');
sb.Append(tourney.EventController.Title);
}
sb.Append(" Tournament Signup");
AddBorderedText(22, 22, 294, 20, sb.ToString().Center(), LabelColor32, BlackColor32);
AddBorderedText(22, 22, 294, 20, sb.AsSpan().Center(), LabelColor32, BlackColor32);
AddBorderedText(
22,
50,

View file

@ -125,7 +125,7 @@ namespace Server.Engines.ConPVP
height - 12 - 2 - 18,
400,
20,
$"Top {lc} of {m_List.Count:N0} duelists, page {page + 1} of {(lc + 14) / 15}".Color(0xFFC000)
Html.Color($"Top {lc} of {m_List.Count:N0} duelists, page {page + 1} of {(lc + 14) / 15}", 0xFFC000)
);
AddColumnHeader(75, "Rank");
@ -174,7 +174,7 @@ namespace Server.Engines.ConPVP
// AddImageTiled( 21, y + 6, width, 8, 0x2617 );
AddImageTiled(x + 3, y + 4, width, 11, 0x806);
AddBorderedText(x, y, 115, level.ToString().Center(), 0xFFFFFF, 0);
AddBorderedText(x, y, 115, Html.Center($"{level}"), 0xFFFFFF, 0);
x += 115;
var mob = entry.Mobile;
@ -189,10 +189,10 @@ namespace Server.Engines.ConPVP
AddBorderedText(x + 5, y, 115 - 5, mob.Name, 0xFFFFFF, 0);
x += 115;
AddBorderedText(x, y, 60, entry.Wins.ToString().Center(), 0xFFFFFF, 0);
AddBorderedText(x, y, 60, Html.Center($"{entry.Wins}"), 0xFFFFFF, 0);
x += 60;
AddBorderedText(x, y, 60, entry.Losses.ToString().Center(), 0xFFFFFF, 0);
AddBorderedText(x, y, 60, Html.Center($"{entry.Losses}"), 0xFFFFFF, 0);
x += 60;
// AddBorderedText( 292 + 15, y, 115 - 30, String.Format( "{0} <DIV ALIGN=CENTER>/</DIV> <DIV ALIGN=RIGHT>{1}</DIV>", entry.Wins, entry.Losses ), 0xFFC000, 0 );

View file

@ -5,6 +5,7 @@ using System.Text;
using Server.Gumps;
using Server.Mobiles;
using Server.Network;
using Server.Text;
namespace Server.Engines.ConPVP
{
@ -51,7 +52,7 @@ namespace Server.Engines.ConPVP
AddPage(0);
AddBackground(0, 0, 300, 300, 9380);
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(128);
if (tourney.TourneyType == TourneyType.FreeForAll)
{
@ -86,12 +87,13 @@ namespace Server.Engines.ConPVP
if (tourney.EventController != null)
{
sb.Append(' ').Append(tourney.EventController.Title);
sb.Append(' ');
sb.Append(tourney.EventController.Title);
}
sb.Append(" Tournament Bracket");
AddHtml(25, 35, 250, 20, sb.ToString().Center());
AddHtml(25, 35, 250, 20, sb.AsSpan().Center());
AddRightArrow(25, 53, ToButtonID(0, 4), "Rules");
AddRightArrow(25, 71, ToButtonID(0, 1), "Participants");
@ -278,7 +280,7 @@ namespace Server.Engines.ConPVP
?? new List<TourneyParticipant>(tourney.Participants);
AddLeftArrow(25, 11, ToButtonID(0, 0));
AddHtml(25, 35, 250, 20, $"{pList.Count} Participant{(pList.Count == 1 ? "" : "s")}".Center());
AddHtml(25, 35, 250, 20, Html.Center($"{pList.Count} Participant{(pList.Count == 1 ? "" : "s")}"));
StartPage(out var index, out var count, out var y, 12);
@ -347,7 +349,7 @@ namespace Server.Engines.ConPVP
AddHtml(25, y, 200, 20, "Log:");
y += 20;
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create();
for (var i = 0; i < part.Log.Count; ++i)
{
@ -364,7 +366,7 @@ namespace Server.Engines.ConPVP
sb.Append("Nothing logged yet.");
}
AddHtml(25, y, 250, 150, sb.ToString().Color(BlackColor32), false, true);
AddHtml(25, y, 250, 150, sb.AsSpan().Color(BlackColor32), false, true);
break;
}

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using Server.Collections;
using Server.Items;
using Server.Mobiles;
@ -272,18 +272,23 @@ namespace Server.Engines.Doom
FacialHairHue = 0x482
};
var items = new List<Item>(dealer.Items);
using var toDelete = PooledRefQueue<Item>.Create();
for (var i = 0; i < items.Count; ++i)
for (var i = 0; i < dealer.Items.Count; ++i)
{
var item = items[i];
var item = dealer.Items[i];
if (item.Layer is not Layer.ShopBuy and not Layer.ShopResale and not Layer.ShopSell)
{
item.Delete();
toDelete.Enqueue(item);
}
}
while (toDelete.Count > 0)
{
toDelete.Dequeue().Delete();
}
dealer.AddItem(new FloppyHat(1));
dealer.AddItem(new Robe(1));
dealer.AddItem(new LanternOfSouls());

View file

@ -667,12 +667,13 @@ public partial class LeverPuzzleController : Item
protected override void OnTick()
{
ticks++;
var mobiles = m_Controller._lampRoom.GetMobiles();
using var mobiles = m_Controller._lampRoom.GetMobilesPooled();
if (ticks >= 71 || m_Controller._lampRoom.GetPlayerCount() == 0)
{
foreach (var mobile in mobiles)
for (var i = 0; i < mobiles.Count; i++)
{
var mobile = mobiles[i];
if (mobile?.Deleted == false && !mobile.IsDeadBondedPet)
{
mobile.Kill();
@ -689,33 +690,36 @@ public partial class LeverPuzzleController : Item
level++;
}
foreach (var mobile in mobiles)
for (var i = 0; i < mobiles.Count; i++)
{
if (IsValidDamagable(mobile))
var mobile = mobiles[i];
if (!IsValidDamagable(mobile))
{
if (ticks % 2 == 0 && level == 5)
continue;
}
if (ticks % 2 == 0 && level == 5)
{
if (mobile.Player)
{
if (mobile.Player)
mobile.Say(1062092);
if (AniSafe(mobile))
{
mobile.Say(1062092);
if (AniSafe(mobile))
{
mobile.Animate(32, 5, 1, true, false, 0);
}
mobile.Animate(32, 5, 1, true, false, 0);
}
DoDamage(mobile, 15, 20, true);
}
if (Utility.Random((int)(level & ~0xfffffffc), 3) == 3)
{
mobile.ApplyPoison(mobile, PA2[level]);
}
DoDamage(mobile, 15, 20, true);
}
if (ticks % 12 == 0 && level > 0 && mobile.Player)
{
mobile.SendLocalizedMessage(PA[level][0], null, PA[level][1]);
}
if (Utility.Random((int)(level & ~0xfffffffc), 3) == 3)
{
mobile.ApplyPoison(mobile, PA2[level]);
}
if (ticks % 12 == 0 && level > 0 && mobile.Player)
{
mobile.SendLocalizedMessage(PA[level][0], null, PA[level][1]);
}
}

View file

@ -32,7 +32,7 @@ public class ElectionManagementGump : Gump
AddHtml(145, 35, 100, 20, (candidate.Mobile == null ? "null" : candidate.Mobile.Name).Color(LabelColor));
AddHtml(45, 55, 100, 20, "Vote Count:".Color(LabelColor));
AddHtml(145, 55, 100, 20, candidate.Votes.ToString().Color(LabelColor));
AddHtml(145, 55, 100, 20, Html.Color($"{candidate.Votes}", LabelColor));
AddButton(12, 73, 4005, 4007, 1);
AddHtml(45, 75, 100, 20, "Drop Candidate".Color(LabelColor));
@ -88,9 +88,9 @@ public class ElectionManagementGump : Gump
AddHtml(x + 2, 140 + idx * 20, 150, 20, mobile.Name.Color(LabelColor));
x += 150;
}
else if (obj is IPAddress)
else if (obj is IPAddress ip)
{
AddHtml(x, 140 + idx * 20, 100, 20, obj.ToString().Center(LabelColor));
AddHtml(x, 140 + idx * 20, 100, 20, Html.Center($"{ip}", LabelColor));
x += 100;
}
else if (obj is DateTime time)
@ -106,7 +106,7 @@ public class ElectionManagementGump : Gump
}
else if (obj is int i1)
{
AddHtml(x, 140 + idx * 20, 60, 20, $"{i1}%".Center(LabelColor));
AddHtml(x, 140 + idx * 20, 60, 20, Html.Center($"{i1}%", LabelColor));
x += 60;
}
}
@ -120,7 +120,7 @@ public class ElectionManagementGump : Gump
AddHtml(10, 10, 268, 20, "Election Management".Center(LabelColor));
AddHtml(45, 35, 100, 20, "Current State:".Color(LabelColor));
AddHtml(145, 35, 100, 20, election.State.ToString().Color(LabelColor));
AddHtml(145, 35, 100, 20, Html.Color($"{election.State}", LabelColor));
AddButton(12, 53, 4005, 4007, 1);
AddHtml(45, 55, 100, 20, "Transition Time:".Color(LabelColor));
@ -147,7 +147,7 @@ public class ElectionManagementGump : Gump
AddButton(13, 118 + i * 20, 4005, 4007, 2 + i);
AddHtml(47, 120 + i * 20, 150, 20, mob.Name.Color(LabelColor));
AddHtml(195, 120 + i * 20, 80, 20, cd.Votes.ToString().Center(LabelColor));
AddHtml(195, 120 + i * 20, 80, 20, Html.Center($"{cd.Votes}", LabelColor));
}
}
}
@ -200,4 +200,4 @@ public class ElectionManagementGump : Gump
}
}
}
}
}

View file

@ -10,7 +10,6 @@ public abstract class BaseFactionTrapDeed : Item, ICraftable
public BaseFactionTrapDeed(int itemID = 0x14F0) : base(itemID)
{
Weight = 1.0;
LootType = LootType.Blessed;
}
@ -18,6 +17,8 @@ public abstract class BaseFactionTrapDeed : Item, ICraftable
{
}
public override double DefaultWeight => 1.0;
public abstract Type TrapType { get; }
[CommandProperty(AccessLevel.GameMaster)]
@ -117,4 +118,4 @@ public abstract class BaseFactionTrapDeed : Item, ICraftable
m_Faction = Faction.ReadReference(reader);
}
}
}

View file

@ -1,5 +1,5 @@
using System;
using System.Collections.Generic;
using Server.Collections;
using Server.Factions.AI;
using Server.Items;
using Server.Mobiles;
@ -234,26 +234,26 @@ namespace Server.Factions
public Mobile FindDispelTarget(bool activeOnly)
{
if (m_Mobile.Deleted || m_Mobile.Int < 95 || CanDispel(m_Mobile) || m_Mobile.AutoDispel)
if (Mobile.Deleted || Mobile.Int < 95 || CanDispel(Mobile) || Mobile.AutoDispel)
{
return null;
}
if (activeOnly)
{
var aggressed = m_Mobile.Aggressed;
var aggressors = m_Mobile.Aggressors;
var aggressed = Mobile.Aggressed;
var aggressors = Mobile.Aggressors;
Mobile active = null;
var activePrio = 0.0;
var comb = m_Mobile.Combatant;
var comb = Mobile.Combatant;
if (comb?.Deleted == false && comb.Alive && !comb.IsDeadBondedPet && m_Mobile.InRange(comb, 12) &&
if (comb?.Deleted == false && comb.Alive && !comb.IsDeadBondedPet && Mobile.InRange(comb, 12) &&
CanDispel(comb))
{
active = comb;
activePrio = m_Mobile.GetDistanceToSqrt(comb);
activePrio = Mobile.GetDistanceToSqrt(comb);
if (activePrio <= 2)
{
@ -266,9 +266,9 @@ namespace Server.Factions
var info = aggressed[i];
var m = info.Defender;
if (m != comb && m.Combatant == m_Mobile && m_Mobile.InRange(m, 12) && CanDispel(m))
if (m != comb && m.Combatant == Mobile && Mobile.InRange(m, 12) && CanDispel(m))
{
var prio = m_Mobile.GetDistanceToSqrt(m);
var prio = Mobile.GetDistanceToSqrt(m);
if (active == null || prio < activePrio)
{
@ -288,9 +288,9 @@ namespace Server.Factions
var info = aggressors[i];
var m = info.Attacker;
if (m != comb && m.Combatant == m_Mobile && m_Mobile.InRange(m, 12) && CanDispel(m))
if (m != comb && m.Combatant == Mobile && Mobile.InRange(m, 12) && CanDispel(m))
{
var prio = m_Mobile.GetDistanceToSqrt(m);
var prio = Mobile.GetDistanceToSqrt(m);
if (active == null || prio < activePrio)
{
@ -308,26 +308,26 @@ namespace Server.Factions
return active;
}
var map = m_Mobile.Map;
var map = Mobile.Map;
if (map != null)
{
Mobile active = null, inactive = null;
double actPrio = 0.0, inactPrio = 0.0;
var comb = m_Mobile.Combatant;
var comb = Mobile.Combatant;
if (comb?.Deleted == false && comb.Alive && !comb.IsDeadBondedPet && CanDispel(comb))
{
active = inactive = comb;
actPrio = inactPrio = m_Mobile.GetDistanceToSqrt(comb);
actPrio = inactPrio = Mobile.GetDistanceToSqrt(comb);
}
foreach (var m in m_Mobile.GetMobilesInRange(12))
foreach (var m in Mobile.GetMobilesInRange(12))
{
if (m != m_Mobile && CanDispel(m))
if (m != Mobile && CanDispel(m))
{
var prio = m_Mobile.GetDistanceToSqrt(m);
var prio = Mobile.GetDistanceToSqrt(m);
if (inactive == null || prio < inactPrio)
{
@ -335,7 +335,7 @@ namespace Server.Factions
inactPrio = prio;
}
if ((m_Mobile.Combatant == m || m.Combatant == m_Mobile) && (active == null || prio < actPrio))
if ((Mobile.Combatant == m || m.Combatant == Mobile) && (active == null || prio < actPrio))
{
active = m;
actPrio = prio;
@ -350,7 +350,7 @@ namespace Server.Factions
}
public bool CanDispel(Mobile m) =>
m is BaseCreature creature && creature.Summoned && m_Mobile.CanBeHarmful(creature, false) &&
m is BaseCreature creature && creature.Summoned && Mobile.CanBeHarmful(creature, false) &&
!creature.IsAnimatedDead;
public void RunTo(Mobile m)
@ -364,14 +364,14 @@ namespace Server.Factions
}
else
{*/
if (!m_Mobile.InRange(m, m_Mobile.RangeFight))
if (!Mobile.InRange(m, Mobile.RangeFight))
{
if (!MoveTo(m, true, 1))
{
OnFailedMove();
}
}
else if (m_Mobile.InRange(m, m_Mobile.RangeFight - 1))
else if (Mobile.InRange(m, Mobile.RangeFight - 1))
{
RunFrom(m);
}
@ -381,7 +381,7 @@ namespace Server.Factions
public void RunFrom(Mobile m)
{
Run((m_Mobile.GetDirectionTo(m) - 4) & Direction.Mask);
Run((Mobile.GetDirectionTo(m) - 4) & Direction.Mask);
}
public void OnFailedMove()
@ -393,36 +393,33 @@ namespace Server.Factions
new TeleportSpell( m_Mobile, null ).Cast();
m_Mobile.DebugSay( "I am stuck, I'm going to try teleporting away" );
DebugSay( "I am stuck, I'm going to try teleporting away" );
}
else*/
if (AcquireFocusMob(m_Mobile.RangePerception, m_Mobile.FightMode, false, false, true))
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
if (m_Mobile.Debug)
{
m_Mobile.DebugSay($"My move is blocked, so I am going to attack {m_Mobile.FocusMob.Name}");
}
this.DebugSayFormatted($"My move is blocked, so I am going to attack {Mobile.FocusMob.Name}");
m_Mobile.Combatant = m_Mobile.FocusMob;
Mobile.Combatant = Mobile.FocusMob;
Action = ActionType.Combat;
}
else if (m_Mobile.Debug)
else
{
m_Mobile.DebugSay("I am stuck");
DebugSay("I am stuck");
}
}
public void Run(Direction d)
{
if (m_Mobile.Spell?.IsCasting == true || m_Mobile.Paralyzed || m_Mobile.Frozen ||
m_Mobile.DisallowAllMoves)
if (Mobile.Spell?.IsCasting == true || Mobile.Paralyzed || Mobile.Frozen ||
Mobile.DisallowAllMoves)
{
return;
}
m_Mobile.Direction = d | Direction.Running;
Mobile.Direction = d | Direction.Running;
if (!DoMove(m_Mobile.Direction, true))
if (!DoMove(Mobile.Direction, true))
{
OnFailedMove();
}
@ -430,7 +427,7 @@ namespace Server.Factions
public override bool Think()
{
if (m_Mobile.Deleted)
if (Mobile.Deleted)
{
return false;
}
@ -438,32 +435,32 @@ namespace Server.Factions
var combatant = m_Guard.Combatant;
if (combatant?.Deleted != false || !combatant.Alive || combatant.IsDeadBondedPet ||
!m_Mobile.CanSee(combatant) || !m_Mobile.CanBeHarmful(combatant, false) || combatant.Map != m_Mobile.Map)
!Mobile.CanSee(combatant) || !Mobile.CanBeHarmful(combatant, false) || combatant.Map != Mobile.Map)
{
// Our combatant is deleted, dead, hidden, or we cannot hurt them
// Try to find another combatant
if (AcquireFocusMob(m_Mobile.RangePerception, m_Mobile.FightMode, false, false, true))
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
m_Mobile.Combatant = combatant = m_Mobile.FocusMob;
m_Mobile.FocusMob = null;
Mobile.Combatant = combatant = Mobile.FocusMob;
Mobile.FocusMob = null;
}
else
{
m_Mobile.Combatant = combatant = null;
Mobile.Combatant = combatant = null;
}
}
if (combatant != null && (!m_Mobile.InLOS(combatant) || !m_Mobile.InRange(combatant, 12)))
if (combatant != null && (!Mobile.InLOS(combatant) || !Mobile.InRange(combatant, 12)))
{
if (AcquireFocusMob(m_Mobile.RangePerception, m_Mobile.FightMode, false, false, true))
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
m_Mobile.Combatant = combatant = m_Mobile.FocusMob;
m_Mobile.FocusMob = null;
Mobile.Combatant = combatant = Mobile.FocusMob;
Mobile.FocusMob = null;
}
else if (!m_Mobile.InRange(combatant, 36))
else if (!Mobile.InRange(combatant, 36))
{
m_Mobile.Combatant = combatant = null;
Mobile.Combatant = combatant = null;
}
}
@ -544,7 +541,7 @@ namespace Server.Factions
Action = ActionType.Combat;
}
m_Mobile.SetCurrentSpeedToActive();
Mobile.SetCurrentSpeedToActive();
m_Guard.Warmode = true;
RunTo(toFollow);
@ -556,7 +553,7 @@ namespace Server.Factions
Action = ActionType.Wander;
}
m_Mobile.SetCurrentSpeedToPassive();
Mobile.SetCurrentSpeedToPassive();
m_Guard.Warmode = false;
WalkRandomInHome(2, 2, 1);
@ -582,9 +579,9 @@ namespace Server.Factions
}
}
var spell = m_Mobile.Spell as Spell;
var spell = Mobile.Spell as Spell;
if (spell == null && Core.TickCount - m_Mobile.NextSpellTime >= 0)
if (spell == null && Core.TickCount - Mobile.NextSpellTime >= 0)
{
var toRelease = DateTime.MinValue;
@ -695,41 +692,42 @@ namespace Server.Factions
var dexMod = GetStatMod(m_Guard, StatType.Dex);
var intMod = GetStatMod(m_Guard, StatType.Int);
var types = new List<Type>();
using var spellTypes = PooledRefQueue<Type>.Create();
if (strMod <= 0)
{
types.Add(typeof(StrengthSpell));
spellTypes.Enqueue(typeof(StrengthSpell));
}
if (dexMod <= 0 && IsAllowed(GuardAI.Melee))
{
types.Add(typeof(AgilitySpell));
spellTypes.Enqueue(typeof(AgilitySpell));
}
if (intMod <= 0 && IsAllowed(GuardAI.Magic))
{
types.Add(typeof(CunningSpell));
spellTypes.Enqueue(typeof(CunningSpell));
}
if (IsAllowed(GuardAI.Bless))
{
if (types.Count > 1)
if (spellTypes.Count > 1)
{
spell = new BlessSpell(m_Guard);
}
else if (types.Count == 1)
else if (spellTypes.Count == 1)
{
spell = types[0].CreateInstance<Spell>(m_Guard, null);
spell = spellTypes.Dequeue().CreateInstance<Spell>(m_Guard, null);
}
}
else if (types.Count > 0)
else if (spellTypes.Count > 0)
{
if (types[0] == typeof(StrengthSpell))
var spellType = spellTypes.Dequeue();
if (spellType == typeof(StrengthSpell))
{
UseItemByType(typeof(BaseStrengthPotion));
}
else if (types[0] == typeof(AgilitySpell))
else if (spellType == typeof(AgilitySpell))
{
UseItemByType(typeof(BaseAgilityPotion));
}
@ -749,30 +747,30 @@ namespace Server.Factions
var dexMod = GetStatMod(combatant, StatType.Dex);
var intMod = GetStatMod(combatant, StatType.Int);
var types = new List<Type>();
using var spellTypes = PooledRefQueue<Type>.Create();
if (strMod >= 0)
{
types.Add(typeof(WeakenSpell));
spellTypes.Enqueue(typeof(WeakenSpell));
}
if (dexMod >= 0 && IsAllowed(GuardAI.Melee))
{
types.Add(typeof(ClumsySpell));
spellTypes.Enqueue(typeof(ClumsySpell));
}
if (intMod >= 0 && IsAllowed(GuardAI.Magic))
{
types.Add(typeof(FeeblemindSpell));
spellTypes.Enqueue(typeof(FeeblemindSpell));
}
if (types.Count > 1)
if (spellTypes.Count > 1)
{
spell = new CurseSpell(m_Guard);
}
else if (types.Count == 1)
else if (spellTypes.Count == 1)
{
spell = types[0].CreateInstance<Spell>(m_Guard, null);
spell = spellTypes.Dequeue().CreateInstance<Spell>(m_Guard, null);
}
}
}

View file

@ -175,6 +175,8 @@ namespace Server.Engines.Harvest
}
}
public override object GetLock(Mobile from, Item tool, HarvestDefinition def, object toHarvest) => this;
public override void OnHarvestStarted(Mobile from, Item tool, HarvestDefinition def, object toHarvest)
{
base.OnHarvestStarted(from, tool, def, toHarvest);

View file

@ -435,6 +435,8 @@ namespace Server.Engines.Harvest
}
}
public override object GetLock(Mobile from, Item tool, HarvestDefinition def, object toHarvest) => this;
public override bool BeginHarvesting(Mobile from, Item tool)
{
if (!base.BeginHarvesting(from, tool))

View file

@ -25,7 +25,7 @@ public sealed class PageResponseGump : StaticGump<PageResponseGump>
// <CENTER><U>Ultima Online Help Response</U></CENTER>
builder.AddHtmlLocalized(150, 40, 360, 40, 1062610);
builder.AddHtml(80, 90, 480, 290, $"{_name} tells {_from.Name}: {_text}", true, true);
builder.AddHtml(80, 90, 480, 290, $"{_name} tells {_from.Name}: {_text}", background: true, scrollbar: true);
// Clicking the OKAY button will remove the response you have received.
builder.AddHtmlLocalized(80, 390, 480, 40, 1062611);

View file

@ -46,7 +46,7 @@ namespace Server.Engines.Help
10,
280,
20,
$"SPEECH LOG - {playerName} (<i>{playerAccount.FixHtmlFormattable()}</i>)".Center(0xA0A0FF)
Html.Center($"SPEECH LOG - {playerName} (<i>{playerAccount.FixHtmlFormattable()}</i>)", 0xA0A0FF)
);
var lastPage = (log.Count - 1) / MaxEntriesPerPage;

View file

@ -118,8 +118,7 @@ public abstract partial class DoneQuestCollector : BaseCreature, IRaceChanger
}
else
{
var conversation = new List<TextDefinition>();
conversation.AddRange(Incomplete);
List<TextDefinition> conversation = [..Incomplete];
var context = MLQuestSystem.GetContext(pm);

View file

@ -1,5 +1,5 @@
using System;
using System.Collections.Generic;
using Server.Collections;
using Server.Gumps;
using Server.Items;
using Server.Logging;
@ -46,7 +46,7 @@ namespace Server.Engines.MLQuests.Objectives
return;
}
var delivery = new List<Item>();
using var delivery = PooledRefQueue<Item>.Create();
for (var i = 0; i < Amount; ++i)
{
@ -54,7 +54,7 @@ namespace Server.Engines.MLQuests.Objectives
if (item != null)
{
delivery.Add(item);
delivery.Enqueue(item);
if (item.Stackable && Amount > 1)
{
@ -64,9 +64,9 @@ namespace Server.Engines.MLQuests.Objectives
}
}
foreach (var item in delivery)
while (delivery.Count > 0)
{
pack.DropItem(item); // Confirmed: on OSI items are added even if your pack is full
pack.DropItem(delivery.Dequeue()); // Confirmed: on OSI items are added even if your pack is full
}
}

View file

@ -1,352 +1,297 @@
using System.Collections;
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: FastAStarAlgorithm.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
************************************************************************/
using System;
using Server.Mobiles;
using CalcMoves = Server.Movement.Movement;
using MoveImpl = Server.Movement.MovementImpl;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Server.PathAlgorithms.FastAStar
namespace Server.PathAlgorithms.FastAStar;
public class FastAStarAlgorithm : PathAlgorithm
{
public struct PathNode
private struct PathNode
{
public int cost, total;
public int parent, next, prev;
public int cost;
public int total;
public int parent;
public int z;
}
public class FastAStarAlgorithm : PathAlgorithm
private const int MaxDepth = 300;
private const int AreaSize = 38;
private const int NodeCount = AreaSize * AreaSize * PlaneCount;
private const int PlaneOffset = 128;
private const int PlaneCount = 13;
private const int PlaneHeight = 20;
public static readonly PathAlgorithm Instance = new FastAStarAlgorithm();
private static readonly Direction[] _path = new Direction[AreaSize * AreaSize];
private static readonly PathNode[] _nodes = new PathNode[NodeCount];
private static readonly byte[] _nodeStates = new byte[NodeCount];
private static readonly int[] _successors = new int[8];
private static readonly PriorityQueue<int, int> _openQueue = new();
private static int _xOffset;
private static int _yOffset;
private Point3D _goal;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Heuristic(int x, int y, int z)
{
private const int MaxDepth = 300;
private const int AreaSize = 38;
x -= _goal.X - _xOffset;
y -= _goal.Y - _yOffset;
z -= _goal.Z;
private const int NodeCount = AreaSize * AreaSize * PlaneCount;
x *= 11;
y *= 11;
private const int PlaneOffset = 128;
private const int PlaneCount = 13;
private const int PlaneHeight = 20;
public static PathAlgorithm Instance = new FastAStarAlgorithm();
return x * x + y * y + z * z;
}
private static readonly Direction[] _path = new Direction[AreaSize * AreaSize];
private static readonly PathNode[] _nodes = new PathNode[NodeCount];
private static readonly BitArray _touched = new(NodeCount);
private static readonly BitArray _onOpen = new(NodeCount);
private static readonly int[] _successors = new int[8];
public override bool CheckCondition(Mobile m, Map map, Point3D start, Point3D goal) =>
Utility.InRange(start, goal, AreaSize);
private static int _xOffset;
private static int _yOffset;
private static int _openList;
private Point3D _goal;
public int Heuristic(int x, int y, int z)
public override Direction[] Find(Mobile m, Map map, Point3D start, Point3D goal)
{
if (!Utility.InRange(start, goal, AreaSize))
{
x -= _goal.X - _xOffset;
y -= _goal.Y - _yOffset;
z -= _goal.Z;
x *= 11;
y *= 11;
return x * x + y * y + z * z;
}
public override bool CheckCondition(Mobile m, Map map, Point3D start, Point3D goal) =>
Utility.InRange(start, goal, AreaSize);
private void RemoveFromChain(int node)
{
if (node is < 0 or >= NodeCount)
{
return;
}
if (!_touched[node] || !_onOpen[node])
{
return;
}
var prev = _nodes[node].prev;
var next = _nodes[node].next;
if (_openList == node)
{
_openList = next;
}
if (prev != -1)
{
_nodes[prev].next = next;
}
if (next != -1)
{
_nodes[next].prev = prev;
}
_nodes[node].prev = -1;
_nodes[node].next = -1;
}
private void AddToChain(int node)
{
if (node is < 0 or >= NodeCount)
{
return;
}
RemoveFromChain(node);
if (_openList != -1)
{
_nodes[_openList].prev = node;
}
_nodes[node].next = _openList;
_nodes[node].prev = -1;
_openList = node;
_touched[node] = true;
_onOpen[node] = true;
}
public override Direction[] Find(Mobile m, Map map, Point3D start, Point3D goal)
{
if (!Utility.InRange(start, goal, AreaSize))
{
return null;
}
_touched.SetAll(false);
_onOpen.SetAll(false);
_goal = goal;
_xOffset = (start.X + goal.X - AreaSize) / 2;
_yOffset = (start.Y + goal.Y - AreaSize) / 2;
var fromNode = GetIndex(start.X, start.Y, start.Z);
var destNode = GetIndex(goal.X, goal.Y, goal.Z);
_openList = fromNode;
_nodes[_openList].cost = 0;
_nodes[_openList].total = Heuristic(start.X - _xOffset, start.Y - _yOffset, start.Z);
_nodes[_openList].parent = -1;
_nodes[_openList].next = -1;
_nodes[_openList].prev = -1;
_nodes[_openList].z = start.Z;
_onOpen[_openList] = true;
_touched[_openList] = true;
var bc = m as BaseCreature;
int backtrack = 0, depth = 0;
var path = _path;
while (_openList != -1)
{
var bestNode = FindBest(_openList);
if (++depth > MaxDepth)
{
break;
}
if (bc != null)
{
MoveImpl.AlwaysIgnoreDoors = bc.CanOpenDoors;
MoveImpl.IgnoreMovableImpassables = bc.CanMoveOverObstacles;
}
MoveImpl.Goal = goal;
var vals = _successors;
var count = GetSuccessors(bestNode, m, map);
MoveImpl.AlwaysIgnoreDoors = false;
MoveImpl.IgnoreMovableImpassables = false;
MoveImpl.Goal = Point3D.Zero;
if (count == 0)
{
break;
}
for (var i = 0; i < count; ++i)
{
var newNode = vals[i];
var wasTouched = _touched[newNode];
if (wasTouched)
{
continue;
}
var newCost = _nodes[bestNode].cost + 1;
var newTotal = newCost + Heuristic(
newNode % AreaSize,
newNode / AreaSize % AreaSize,
_nodes[newNode].z
);
_nodes[newNode].parent = bestNode;
_nodes[newNode].cost = newCost;
_nodes[newNode].total = newTotal;
if (_onOpen[newNode])
{
continue;
}
AddToChain(newNode);
if (newNode != destNode)
{
continue;
}
var pathCount = 0;
var parent = _nodes[newNode].parent;
while (parent != -1)
{
path[pathCount++] = GetDirection(
parent % AreaSize,
parent / AreaSize % AreaSize,
newNode % AreaSize,
newNode / AreaSize % AreaSize
);
newNode = parent;
parent = _nodes[newNode].parent;
if (newNode == fromNode)
{
break;
}
}
var dirs = new Direction[pathCount];
while (pathCount > 0)
{
dirs[backtrack++] = path[--pathCount];
}
return dirs;
}
}
return null;
}
private int GetIndex(int x, int y, int z)
Array.Clear(_nodeStates);
_goal = goal;
_xOffset = (start.X + goal.X - AreaSize) / 2;
_yOffset = (start.Y + goal.Y - AreaSize) / 2;
var fromNode = GetIndex(start.X, start.Y, start.Z);
var destNode = GetIndex(goal.X, goal.Y, goal.Z);
_nodes[fromNode].cost = 0;
_nodes[fromNode].total = Heuristic(start.X - _xOffset, start.Y - _yOffset, start.Z);
_nodes[fromNode].parent = -1;
_nodes[fromNode].z = start.Z;
_openQueue.Enqueue(fromNode, _nodes[fromNode].total);
_nodeStates[fromNode] = 1;
var bc = m as BaseCreature;
int backtrack = 0, depth = 0;
var path = _path;
while (_openQueue.Count > 0)
{
x -= _xOffset;
y -= _yOffset;
z += PlaneOffset;
z /= PlaneHeight;
return x + y * AreaSize + z * AreaSize * AreaSize;
}
private int FindBest(int node)
{
var least = _nodes[node].total;
var leastNode = node;
while (node != -1)
if (++depth > MaxDepth)
{
if (_nodes[node].total < least)
{
least = _nodes[node].total;
leastNode = node;
}
node = _nodes[node].next;
break;
}
RemoveFromChain(leastNode);
if (!_openQueue.TryDequeue(out var bestNode, out var bestTotal))
{
break;
}
_touched[leastNode] = true;
_onOpen[leastNode] = false;
// Duplicate, lower priority
if (_nodeStates[bestNode] == 2 || _nodes[bestNode].total != bestTotal)
{
continue;
}
return leastNode;
}
_nodeStates[bestNode] = 2;
public int GetSuccessors(int p, Mobile m, Map map)
{
var px = p % AreaSize;
var py = p / AreaSize % AreaSize;
var pz = _nodes[p].z;
if (bc != null)
{
MoveImpl.AlwaysIgnoreDoors = bc.CanOpenDoors;
MoveImpl.IgnoreMovableImpassables = bc.CanMoveOverObstacles;
}
var p3D = new Point3D(px + _xOffset, py + _yOffset, pz);
MoveImpl.Goal = goal;
var vals = _successors;
var count = 0;
var count = GetSuccessors(bestNode, m, map);
for (var i = 0; i < 8; ++i)
MoveImpl.AlwaysIgnoreDoors = false;
MoveImpl.IgnoreMovableImpassables = false;
MoveImpl.Goal = Point3D.Zero;
if (count == 0)
{
int x;
int y;
switch (i)
{
default: // 0
x = 0;
y = -1;
break;
case 1:
x = 1;
y = -1;
break;
case 2:
x = 1;
y = 0;
break;
case 3:
x = 1;
y = 1;
break;
case 4:
x = 0;
y = 1;
break;
case 5:
x = -1;
y = 1;
break;
case 6:
x = -1;
y = 0;
break;
case 7:
x = -1;
y = -1;
break;
}
continue;
}
x += px;
y += py;
for (var i = 0; i < count; ++i)
{
var newNode = vals[i];
if (x is < 0 or >= AreaSize || y is < 0 or >= AreaSize)
// Skip if the node is already closed
if (_nodeStates[newNode] == 2)
{
continue;
}
if (CalcMoves.CheckMovement(m, map, p3D, (Direction)i, out var z))
{
var idx = GetIndex(x + _xOffset, y + _yOffset, z);
var isDiagonal = i % 2 == 1;
var moveCost = isDiagonal ? 14 : 10;
var newCost = _nodes[bestNode].cost + moveCost;
var newTotal = newCost + Heuristic(
newNode % AreaSize,
newNode / AreaSize % AreaSize,
_nodes[newNode].z
);
if (idx >= 0 && idx < NodeCount)
if (_nodeStates[newNode] == 0 || newTotal < _nodes[newNode].total)
{
_nodes[newNode].parent = bestNode;
_nodes[newNode].cost = newCost;
_nodes[newNode].total = newTotal;
// Requeue (duplicates allowed), and mark as open
_openQueue.Enqueue(newNode, newTotal);
_nodeStates[newNode] = 1;
}
if (newNode != destNode)
{
continue;
}
var pathCount = 0;
var parent = _nodes[newNode].parent;
while (parent != -1)
{
path[pathCount++] = GetDirection(
parent % AreaSize,
parent / AreaSize % AreaSize,
newNode % AreaSize,
newNode / AreaSize % AreaSize
);
newNode = parent;
parent = _nodes[newNode].parent;
if (newNode == fromNode)
{
_nodes[idx].z = z;
vals[count++] = idx;
break;
}
}
var dirs = new Direction[pathCount];
while (pathCount > 0)
{
dirs[backtrack++] = path[--pathCount];
}
_openQueue.Clear();
return dirs;
}
}
_openQueue.Clear();
return null;
}
private static int GetIndex(int x, int y, int z)
{
x -= _xOffset;
y -= _yOffset;
z += PlaneOffset;
z /= PlaneHeight;
return x + y * AreaSize + z * AreaSize * AreaSize;
}
private static int GetSuccessors(int p, Mobile m, Map map)
{
var px = p % AreaSize;
var py = p / AreaSize % AreaSize;
var pz = _nodes[p].z;
var p3D = new Point3D(px + _xOffset, py + _yOffset, pz);
var vals = _successors;
var count = 0;
for (var i = 0; i < 8; ++i)
{
int x;
int y;
switch (i)
{
default: // 0
x = 0;
y = -1;
break;
case 1:
x = 1;
y = -1;
break;
case 2:
x = 1;
y = 0;
break;
case 3:
x = 1;
y = 1;
break;
case 4:
x = 0;
y = 1;
break;
case 5:
x = -1;
y = 1;
break;
case 6:
x = -1;
y = 0;
break;
case 7:
x = -1;
y = -1;
break;
}
return count;
x += px;
y += py;
if (x is < 0 or >= AreaSize || y is < 0 or >= AreaSize)
{
continue;
}
if (CalcMoves.CheckMovement(m, map, p3D, (Direction)i, out var z))
{
var idx = GetIndex(x + _xOffset, y + _yOffset, z);
if (idx >= 0 && idx < NodeCount)
{
_nodes[idx].z = z;
vals[count++] = idx;
}
}
}
return count;
}
}

View file

@ -9,7 +9,8 @@ public partial class FertileDirt : Item
public FertileDirt(int amount = 1) : base(0xF81)
{
Stackable = true;
Weight = 1.0;
Amount = amount;
}
public override double DefaultWeight => 1.0;
}

View file

@ -12,11 +12,12 @@ public partial class GreenThorns : Item
public GreenThorns(int amount = 1) : base(0xF42)
{
Stackable = true;
Weight = 1.0;
Hue = 0x42;
Amount = amount;
}
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1060837; // green thorns
public override void OnDoubleClick(Mobile from)

View file

@ -56,7 +56,11 @@ public partial class PlantBowl : Item
};
[Constructible]
public PlantBowl() : base(0x15FD) => Weight = 1.0;
public PlantBowl() : base(0x15FD)
{
}
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1060834; // a plant bowl

View file

@ -56,8 +56,6 @@ public partial class PlantItem : Item, ISecurable
[Constructible]
public PlantItem(bool fertileDirt = false) : base(0x1602)
{
Weight = 1.0;
_plantStatus = PlantStatus.BowlOfDirt;
_plantSystem = new PlantSystem(this)
{
@ -69,6 +67,8 @@ public partial class PlantItem : Item, ISecurable
Plants.Add(this);
}
public override double DefaultWeight => 1.0;
public ObjectPropertyList OldClientPropertyList
{
get

View file

@ -320,7 +320,10 @@ namespace Server.Engines.Plants
}
[SerializableFieldSaveFlag(17)]
private bool ShouldSerializeLeftSeeds() => _leftSeeds != 0;
private bool ShouldSerializeLeftSeeds() => _leftSeeds != 8;
[SerializableFieldDefault(17)]
private int LeftSeedsDefaultValue() => 8;
[SerializableProperty(18)]
public int AvailableResources
@ -340,7 +343,10 @@ namespace Server.Engines.Plants
}
[SerializableFieldSaveFlag(19)]
private bool ShouldSerializeLeftResources() => _leftResources != 0;
private bool ShouldSerializeLeftResources() => _leftResources != 8;
[SerializableFieldDefault(19)]
private int LeftResourcesDefaultValue() => 8;
public void Reset(bool potions)
{

View file

@ -24,7 +24,6 @@ public partial class Seed : Item
[Constructible]
public Seed(PlantType plantType, PlantHue plantHue, bool showType = false) : base(0xDCF)
{
Weight = 1.0;
Stackable = Core.SA;
_plantType = plantType;
@ -34,6 +33,8 @@ public partial class Seed : Item
Hue = PlantHueInfo.GetInfo(plantHue).Hue;
}
public override double DefaultWeight => 1.0;
[CommandProperty(AccessLevel.GameMaster)]
[SerializableProperty(1)]
public PlantHue PlantHue

View file

@ -8,11 +8,9 @@ namespace Server.Engines.Quests.Collector;
public partial class EnchantedPaints : QuestItem
{
[Constructible]
public EnchantedPaints() : base(0xFC1)
{
LootType = LootType.Blessed;
Weight = 1.0;
}
public EnchantedPaints() : base(0xFC1) => LootType = LootType.Blessed;
public override double DefaultWeight => 1.0;
public override bool CanDrop(PlayerMobile player) => player.Quest is not CollectorQuest;

View file

@ -14,12 +14,12 @@ public partial class PaintedImage : Item
[Constructible]
public PaintedImage(ImageType image) : base(0xFF3)
{
Weight = 1.0;
Hue = 0x8FD;
_image = image;
}
public override double DefaultWeight => 1.0;
public override void AddNameProperty(IPropertyList list)
{
var info = ImageTypeInfo.Get(_image);

View file

@ -73,7 +73,11 @@ public partial class EnchantedSextant : Item
};
[Constructible]
public EnchantedSextant() : base(0x1058) => Weight = 2.0;
public EnchantedSextant() : base(0x1058)
{
}
public override double DefaultWeight => 2.0;
public override int LabelNumber => 1046226; // an enchanted sextant

View file

@ -27,10 +27,11 @@ public partial class HornOfRetreat : Item
public HornOfRetreat() : base(0xFC4)
{
Hue = 0x482;
Weight = 1.0;
_charges = 10;
}
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1049117; // Horn of Retreat
public virtual bool ValidateUse(Mobile from) => true;

View file

@ -693,7 +693,7 @@ namespace Server.Engines.Quests
}
else
{
AddHtml(x, y, width, height, message.ToString().Color(color.C16216()), back, scroll);
AddHtml(x, y, width, height, Html.Color($"{message}", color.C16216()), back, scroll);
}
}
}

View file

@ -12,11 +12,9 @@ public partial class KronusScroll : QuestItem
private static readonly Map m_WellOfTearsMap = Map.Malas;
[Constructible]
public KronusScroll() : base(0x227A)
{
Weight = 1.0;
Hue = 0x44E;
}
public KronusScroll() : base(0x227A) => Hue = 0x44E;
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1060149; // Calling of Kronus

View file

@ -8,7 +8,11 @@ namespace Server.Engines.Quests.Necro;
public partial class ScrollOfAbraxus : QuestItem
{
[Constructible]
public ScrollOfAbraxus() : base(0x227B) => Weight = 1.0;
public ScrollOfAbraxus() : base(0x227B)
{
}
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1028827; // Scroll of Abraxus

View file

@ -86,7 +86,7 @@ public partial class SummonedPaladin : BaseCreature
Timer.StartTimer(TimeSpan.FromSeconds(5.0), Delete);
}
else if (_necromancer.Map != Map || GetDistanceToSqrt(_necromancer) > RangePerception + 1)
else if (_necromancer.Map != Map || this.GetDistanceToSqrt(_necromancer) > RangePerception + 1)
{
Effects.SendLocationParticles(
EffectItem.Create(Location, Map, EffectItem.DefaultDuration),

View file

@ -7,7 +7,11 @@ namespace Server.Engines.Quests.Ninja;
public partial class EminosKatana : QuestItem
{
[Constructible]
public EminosKatana() : base(0x13FF) => Weight = 1.0;
public EminosKatana() : base(0x13FF)
{
}
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1063214; // Daimyo Emino's Katana

View file

@ -7,11 +7,9 @@ namespace Server.Engines.Quests.Ninja;
public partial class NoteForZoel : QuestItem
{
[Constructible]
public NoteForZoel() : base(0x14EF)
{
Weight = 1.0;
Hue = 0x6B9;
}
public NoteForZoel() : base(0x14EF) => Hue = 0x6B9;
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1063186; // A Note for Zoel

View file

@ -7,7 +7,11 @@ namespace Server.Engines.Quests.Samurai;
public partial class HaochisKatana : QuestItem
{
[Constructible]
public HaochisKatana() : base(0x13FF) => Weight = 1.0;
public HaochisKatana() : base(0x13FF)
{
}
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1063165; // Daimyo Haochi's Katana

View file

@ -8,10 +8,11 @@ public partial class GoldenSkull : Item
[Constructible]
public GoldenSkull() : base(Utility.Random(0x1AE2, 3))
{
Weight = 1.0;
Hue = 0x8A5;
LootType = LootType.Blessed;
}
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1061619; // a golden skull
}

View file

@ -8,11 +8,12 @@ public partial class GrandGrimoire : Item
[Constructible]
public GrandGrimoire() : base(0xEFA)
{
Weight = 1.0;
Hue = 0x835;
Layer = Layer.OneHanded;
LootType = LootType.Blessed;
}
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1060801; // The Grand Grimoire
}

View file

@ -7,7 +7,11 @@ namespace Server.Engines.Quests.Haven;
public partial class QuestDaemonBlood : QuestItem
{
[Constructible]
public QuestDaemonBlood() : base(0xF7D) => Weight = 1.0;
public QuestDaemonBlood() : base(0xF7D)
{
}
public override double DefaultWeight => 1.0;
public override bool CanDrop(PlayerMobile player) => player.Quest is not UzeraanTurmoilQuest;
}

View file

@ -7,7 +7,11 @@ namespace Server.Engines.Quests.Haven;
public partial class QuestDaemonBone : QuestItem
{
[Constructible]
public QuestDaemonBone() : base(0xF80) => Weight = 1.0;
public QuestDaemonBone() : base(0xF80)
{
}
public override double DefaultWeight => 1.0;
public override bool CanDrop(PlayerMobile player) => player.Quest is not UzeraanTurmoilQuest;
}

View file

@ -7,7 +7,11 @@ namespace Server.Engines.Quests.Haven;
public partial class QuestFertileDirt : QuestItem
{
[Constructible]
public QuestFertileDirt() : base(0xF81) => Weight = 1.0;
public QuestFertileDirt() : base(0xF81)
{
}
public override double DefaultWeight => 1.0;
public override bool CanDrop(PlayerMobile player) => player.Quest is not UzeraanTurmoilQuest;
}

View file

@ -6,11 +6,9 @@ namespace Server.Engines.Quests.Haven;
[SerializationGenerator(0, false)]
public partial class SchmendrickScrollOfPower : QuestItem
{
public SchmendrickScrollOfPower() : base(0xE34)
{
Weight = 1.0;
Hue = 0x34D;
}
public SchmendrickScrollOfPower() : base(0xE34) => Hue = 0x34D;
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1049118; // a scroll with ancient markings

View file

@ -6,7 +6,11 @@ namespace Server.Items;
public partial class Cauldron : Item
{
[Constructible]
public Cauldron() : base(0x9ED) => Weight = 1.0;
public Cauldron() : base(0x9ED)
{
}
public override double DefaultWeight => 1.0;
public override string DefaultName => "a cauldron";
}

View file

@ -13,12 +13,13 @@ public partial class HangoverCure : Item
[Constructible]
public HangoverCure() : base(0xE2B)
{
Weight = 1.0;
Hue = 0x2D;
_uses = 20;
}
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1055060; // Grizelda's Extra Strength Hangover Cure
public override void OnDoubleClick(Mobile from)

View file

@ -6,7 +6,11 @@ namespace Server.Engines.Quests.Hag;
public partial class MoonfireBrew : Item
{
[Constructible]
public MoonfireBrew() : base(0xF04) => Weight = 1.0;
public MoonfireBrew() : base(0xF04)
{
}
public override double DefaultWeight => 1.0;
public override int LabelNumber => 1055065; // a bottle of magical moonfire brew
}

View file

@ -167,8 +167,8 @@ public class SpawnerGump : Gump
totalWeight += spawnerEntry.SpawnedProbability;
}
AddHtml(270, 308 + offset, 35, 20, totalSpawns.ToString().Center(0xF4F4F4));
AddHtml(308, 308 + offset, 35, 20, totalWeight.ToString().Center(0xF4F4F4));
AddHtml(270, 308 + offset, 35, 20, Html.Center($"{totalSpawns}", 0xF4F4F4));
AddHtml(308, 308 + offset, 35, 20, Html.Center($"{totalWeight}",0xF4F4F4));
AddHtml(5, 1, 161, 20, $"<BASEFONT COLOR=#FFEA00>{spawner.Name}</BASEFONT><BASEFONT COLOR={GetCountColor(totalSpawned, spawner.Count)}> ({totalSpawned}/{spawner.Count})</BASEFONT>");

Some files were not shown because too many files have changed in this diff Show more