Merge branch 'main' into Refactor/MagerySpell.cs
This commit is contained in:
commit
adbb6e272f
712 changed files with 17498 additions and 9146 deletions
|
|
@ -3,7 +3,7 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"modernuoschemagenerator": {
|
||||
"version": "2.12.20",
|
||||
"version": "2.13.0",
|
||||
"commands": [
|
||||
"ModernUOSchemaGenerator"
|
||||
]
|
||||
|
|
|
|||
18
.github/workflows/build-test.yml
vendored
18
.github/workflows/build-test.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/create-release.yml
vendored
2
.github/workflows/create-release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
597
Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs
Normal file
597
Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
549
Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs
Normal file
549
Projects/Server.Tests/Tests/Maps/ClientEnumeratorTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
484
Projects/Server.Tests/Tests/Maps/ItemEnumeratorTests.cs
Normal file
484
Projects/Server.Tests/Tests/Maps/ItemEnumeratorTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
263
Projects/Server.Tests/Tests/Maps/MobileEnumeratorTests.cs
Normal file
263
Projects/Server.Tests/Tests/Maps/MobileEnumeratorTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
412
Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs
Normal file
412
Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
319
Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs
Normal file
319
Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs
Normal 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)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
242
Projects/Server.Tests/Tests/Utility/HtmlEscapeTests.cs
Normal file
242
Projects/Server.Tests/Tests/Utility/HtmlEscapeTests.cs
Normal 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("<", "<")]
|
||||
[InlineData(">", ">")]
|
||||
[InlineData("&", "&")]
|
||||
[InlineData("\"", """)]
|
||||
[InlineData("'", "'")]
|
||||
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("<><>", "<><>")]
|
||||
[InlineData("&&&&", "&&&&")]
|
||||
[InlineData("\"\"\"", """"")]
|
||||
[InlineData("'''", "'''")]
|
||||
[InlineData(">>>", ">>>")]
|
||||
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 <world>")]
|
||||
[InlineData("<div>Hello</div>", "<div>Hello</div>")]
|
||||
[InlineData("Tom & Jerry", "Tom & Jerry")]
|
||||
[InlineData("He said \"hello\"", "He said "hello"")]
|
||||
[InlineData("It's a test", "It'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", "<Hello")]
|
||||
[InlineData(">World", ">World")]
|
||||
[InlineData("&Start", "&Start")]
|
||||
[InlineData("\"Quote", ""Quote")]
|
||||
[InlineData("'Apostrophe", "'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<")]
|
||||
[InlineData("World>", "World>")]
|
||||
[InlineData("End&", "End&")]
|
||||
[InlineData("Quote\"", "Quote"")]
|
||||
[InlineData("Test'", "Test'")]
|
||||
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>", "<p>Hello & goodbye</p>")]
|
||||
[InlineData("<already>", "&lt;already&gt;")]
|
||||
[InlineData("<tag attr=\"value\" data='test'>", "<tag attr="value" data='test'>")]
|
||||
[InlineData("a<b>c&d\"e'f", "a<b>c&d"e'f")]
|
||||
[InlineData(" ", "&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 = "<>&"'";
|
||||
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 &
|
||||
// If we're not careful, we could double-escape already-escaped content
|
||||
const string input = "<";
|
||||
const string expected = "&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>", "<div>")]
|
||||
[InlineData("Tom & Jerry", "Tom & 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 = "<<>>&&""''";
|
||||
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 = "<a>b&c"d'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 = "<html><body>Hello</body></html>";
|
||||
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 = "<a href="test" data='value'>";
|
||||
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("<", 100))}End{string.Join("", Enumerable.Repeat("&", 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 世界 <test> & 🎉";
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
299
Projects/Server/Maps/Map.ClientByDistanceEnumerator.cs
Normal file
299
Projects/Server/Maps/Map.ClientByDistanceEnumerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
325
Projects/Server/Maps/Map.ItemByDistanceEnumerator.cs
Normal file
325
Projects/Server/Maps/Map.ItemByDistanceEnumerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
320
Projects/Server/Maps/Map.MobileByDistanceEnumerator.cs
Normal file
320
Projects/Server/Maps/Map.MobileByDistanceEnumerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
63
Projects/Server/Mobiles/Mobile.Enumerators.cs
Normal file
63
Projects/Server/Mobiles/Mobile.Enumerators.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
'&' => "&",
|
||||
'<' => "<",
|
||||
'>' => ">",
|
||||
'"' => """,
|
||||
'\'' => "'"
|
||||
};
|
||||
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("<", "<")
|
||||
.Replace(">", ">")
|
||||
.Replace("&", "&")
|
||||
.Replace("\"", """)
|
||||
.Replace("'", "'")
|
||||
.ToString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
299
Projects/UOContent.Tests/Tests/Skills/TrackingTests.cs
Normal file
299
Projects/UOContent.Tests/Tests/Skills/TrackingTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)..]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue