diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index eb74c39a4..d9219a87f 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -57,7 +57,7 @@ jobs:
# blame-hang kills a stuck test host after 10 minutes and reports the in-flight
# tests plus a process dump instead of hanging until the job timeout.
run: |
- dotnet test --logger trx --results-directory ./TestResults --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type full
+ dotnet test --filter "Category!=Manual" --logger trx --results-directory ./TestResults --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type full
if [ -z "$(find ./TestResults -name '*.trx' 2>/dev/null)" ]; then
echo "::error::No test result files were produced - no test projects ran. Failing to avoid masking failures."
exit 1
@@ -167,7 +167,7 @@ jobs:
# blame-hang kills a stuck test host after 10 minutes and reports the in-flight
# tests plus a process dump instead of hanging until the job timeout.
run: |
- dotnet test --logger trx --results-directory ./TestResults --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type full
+ dotnet test --filter "Category!=Manual" --logger trx --results-directory ./TestResults --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type full
if [ -z "$(find ./TestResults -name '*.trx' 2>/dev/null)" ]; then
echo "::error::No test result files were produced - no test projects ran. Failing to avoid masking failures."
exit 1
diff --git a/Projects/Server.Tests/Helpers/EntityCleanup.cs b/Projects/Server.Tests/Helpers/EntityCleanup.cs
new file mode 100644
index 000000000..61917b61e
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/EntityCleanup.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+
+namespace Server.Tests;
+
+///
+/// Deletes tracked entities on dispose, so a test that fails mid-way does not leave them in the shared World.
+/// Null and already-deleted entities are skipped.
+///
+internal sealed class EntityCleanup : IDisposable
+{
+ private readonly List _entities = [];
+
+ public T Track(T entity) where T : IEntity
+ {
+ _entities.Add(entity);
+ return entity;
+ }
+
+ public void Dispose()
+ {
+ // Reverse order deletes children before the containers or owners that hold them.
+ for (var i = _entities.Count - 1; i >= 0; i--)
+ {
+ if (_entities[i] is { Deleted: false } entity)
+ {
+ entity.Delete();
+ }
+ }
+
+ _entities.Clear();
+ }
+}
diff --git a/Projects/Server.Tests/Helpers/MockAccount.cs b/Projects/Server.Tests/Helpers/MockAccount.cs
index 8d8973d86..2725d3438 100644
--- a/Projects/Server.Tests/Helpers/MockAccount.cs
+++ b/Projects/Server.Tests/Helpers/MockAccount.cs
@@ -20,15 +20,27 @@ public class MockAccount : IAccount
public string Username { get; }
public string Email { get; set; }
public AccessLevel AccessLevel { get; set; }
- public int Length { get; }
+ public int Length { get; private set; }
public int Limit { get; set; } = 6; // Default to 6 character slots
- public int Count { get; }
+ public int Count { get; private set; }
private readonly Dictionary _mobiles = new();
public Mobile this[int index]
{
get => _mobiles.GetValueOrDefault(index);
- set => _mobiles[index] = value;
+ set
+ {
+ var old = _mobiles.GetValueOrDefault(index);
+ _mobiles[index] = value;
+
+ Length = Math.Max(Length, index + 1);
+ Count += (value != null ? 1 : 0) - (old != null ? 1 : 0);
+
+ if (value != null)
+ {
+ value.Account = this;
+ }
+ }
}
public DateTime Created { get; set; }
diff --git a/Projects/Server.Tests/Helpers/NetTestHelpers.cs b/Projects/Server.Tests/Helpers/NetTestHelpers.cs
new file mode 100644
index 000000000..3f3cef3fc
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/NetTestHelpers.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Net.Sockets;
+using System.Network;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using Server.Network;
+using Xunit;
+
+namespace Server.Tests.Network;
+
+///
+/// Shared plumbing for tests that drive a real over loopback.
+///
+public static class NetTestHelpers
+{
+ public const byte PingPacketId = 0x73;
+
+ /// The payload byte of the last ping delivered to the handler from .
+ public static byte LastPing { get; private set; }
+
+ ///
+ /// Restores the handler that was in a packet slot before a test replaced it. The handler table is
+ /// process-global, so a handler left behind would silently answer for that id in every later test.
+ ///
+ public readonly struct HandlerRegistration : IDisposable
+ {
+ private readonly int _packetId;
+ private readonly PacketHandler _previous;
+
+ internal HandlerRegistration(int packetId, PacketHandler previous)
+ {
+ _packetId = packetId;
+ _previous = previous;
+ }
+
+ public void Dispose() => IncomingPackets.Handlers[_packetId] = _previous;
+ }
+
+ public static unsafe HandlerRegistration RegisterHandler(
+ int packetId, int length, delegate* onReceive
+ )
+ {
+ var previous = IncomingPackets.GetHandler(packetId);
+ IncomingPackets.Register(packetId, length, false, onReceive);
+ return new HandlerRegistration(packetId, previous);
+ }
+
+ /// Registers a two-byte ping whose payload byte is recorded in .
+ public static unsafe HandlerRegistration RegisterPing()
+ {
+ LastPing = 0;
+ return RegisterHandler(PingPacketId, 2, &RecordPing);
+ }
+
+ private static void RecordPing(NetState state, SpanReader reader) => LastPing = reader.ReadByte();
+
+ public static byte[] Pattern(int length, int seed)
+ {
+ var data = new byte[length];
+ new System.Random(seed).NextBytes(data);
+ return data;
+ }
+
+ ///
+ /// Pre-auth buffers are off only where the platform floor leaves no room below the base size
+ /// (the Windows Server 2012/2016 mapping path, or a 64 KiB-page kernel). Anywhere else a zero
+ /// initial size is a configuration regression and fails rather than skips.
+ ///
+ public static void RequirePreAuthBuffers()
+ {
+ var floor = IORingBuffer.MinimumSize;
+ Skip.If(
+ floor >= NetState.RecvBufferSize || floor >= NetState.SendBufferSize,
+ $"Pre-auth buffers are off on this platform: the {floor}-byte mapping floor leaves no room below the base buffers (recv {NetState.RecvBufferSize}, send {NetState.SendBufferSize})"
+ );
+
+ Assert.True(
+ NetState.InitialRecvBufferSize > 0 && NetState.InitialSendBufferSize > 0,
+ $"Pre-auth buffers are off although the {floor}-byte platform floor allows them (initial recv {NetState.InitialRecvBufferSize}, send {NetState.InitialSendBufferSize})"
+ );
+ }
+
+ /// Runs the network slice until holds, failing with the condition that timed out.
+ public static void SliceUntil(
+ Func done, int timeoutMs = 5000, [CallerArgumentExpression(nameof(done))] string condition = null
+ )
+ {
+ var deadline = Stopwatch.StartNew();
+ while (!done())
+ {
+ if (deadline.ElapsedMilliseconds >= timeoutMs)
+ {
+ Assert.Fail($"Timed out after {timeoutMs} ms waiting for {condition}");
+ }
+
+ NetState.Slice();
+ Thread.Sleep(5);
+ }
+ }
+
+ /// Reads exactly bytes from the peer, slicing the server while it waits.
+ public static byte[] ReadAll(Socket client, int length, int timeoutMs = 10000)
+ {
+ var received = new byte[length];
+ var total = 0;
+ var deadline = Stopwatch.StartNew();
+ while (total < length)
+ {
+ if (deadline.ElapsedMilliseconds >= timeoutMs)
+ {
+ Assert.Fail($"Timed out after {timeoutMs} ms with {total} of {length} bytes received by the peer");
+ }
+
+ NetState.Slice();
+
+ // Poll takes microseconds, not milliseconds
+ if (client.Poll(50_000, SelectMode.SelectRead))
+ {
+ var read = client.Receive(received, total, length - total, SocketFlags.None);
+ Assert.True(read != 0, $"Peer saw EOF after {total} of {length} bytes");
+ total += read;
+ }
+ }
+
+ return received;
+ }
+
+ /// Waits for the server's FIN to reach the peer, failing if any data arrives first.
+ public static void WaitForEof(Socket client, int timeoutMs = 10000)
+ {
+ var scratch = new byte[64];
+ var deadline = Stopwatch.StartNew();
+ while (true)
+ {
+ if (deadline.ElapsedMilliseconds >= timeoutMs)
+ {
+ Assert.Fail($"Timed out after {timeoutMs} ms waiting for the peer to see EOF");
+ }
+
+ NetState.Slice();
+
+ if (client.Poll(50_000, SelectMode.SelectRead))
+ {
+ var read = client.Receive(scratch);
+ Assert.True(read == 0, $"Peer received {read} unexpected bytes while waiting for EOF");
+ return;
+ }
+ }
+ }
+}
diff --git a/Projects/Server.Tests/Helpers/PacketAssert.cs b/Projects/Server.Tests/Helpers/PacketAssert.cs
new file mode 100644
index 000000000..ca8efb5ee
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/PacketAssert.cs
@@ -0,0 +1,31 @@
+using System;
+using Server.Network;
+
+namespace Server.Tests.Network;
+
+public static class PacketAssert
+{
+ ///
+ /// Asserts that writes exactly the bytes of the reference
+ /// packet to a fresh test NetState negotiated with .
+ ///
+ public static void Sends(Packet expected, Action send, ProtocolChanges changes = ProtocolChanges.None) =>
+ Sends(_ => expected, send, changes);
+
+ ///
+ /// As , for a reference packet that depends on
+ /// the negotiated NetState. It is built after is applied and before the send.
+ ///
+ public static void Sends(
+ Func expected, Action send, ProtocolChanges changes = ProtocolChanges.None
+ )
+ {
+ using var ns = PacketTestUtilities.CreateTestNetState();
+ ns.ProtocolChanges = changes;
+
+ var expectedBytes = expected(ns).Compile();
+ send(ns);
+
+ AssertThat.Equal(ns.SendBuffer.GetReadSpan(), expectedBytes);
+ }
+}
diff --git a/Projects/Server.Tests/Helpers/PersistencePipelineScope.cs b/Projects/Server.Tests/Helpers/PersistencePipelineScope.cs
new file mode 100644
index 000000000..dbdb05524
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/PersistencePipelineScope.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+
+namespace Server.Tests;
+
+///
+/// A private save pipeline: a chunk source with its own serialization workers, optionally installed as
+/// World's workers, optionally a scratch directory and an extra assembly visible to the type loader.
+/// Dispose unregisters every tracked persistence, exits the workers and restores every static it swapped.
+///
+internal sealed class PersistencePipelineScope : IDisposable
+{
+ private readonly List _persistences = [];
+ private readonly SerializationThreadWorker[] _previousWorkers;
+ private readonly Assembly[] _previousAssemblies;
+ private readonly DateTime _previousSaveStart;
+ private readonly bool _installedWorkers;
+ private readonly bool _swappedAssemblies;
+
+ /// Workers to create, each with an allocated heap.
+ /// Install the workers as World's, which WriteSnapshot reads segments from.
+ /// An assembly the loader must resolve types from by hash.
+ /// Creates a unique scratch directory under the temp path when set.
+ public PersistencePipelineScope(
+ int workerCount,
+ bool installWorkers = false,
+ Assembly visibleAssembly = null,
+ string directoryPrefix = null
+ )
+ {
+ _previousSaveStart = World.SaveStartTime;
+
+ Source = new SerializationChunkSource();
+ Workers = new SerializationThreadWorker[workerCount];
+ for (var i = 0; i < Workers.Length; i++)
+ {
+ Workers[i] = new SerializationThreadWorker(i, Source);
+ Workers[i].AllocateHeap();
+ }
+
+ if (installWorkers)
+ {
+ _installedWorkers = true;
+ _previousWorkers = World._threadWorkers;
+ World._threadWorkers = Workers;
+ }
+
+ if (visibleAssembly != null)
+ {
+ _swappedAssemblies = true;
+ _previousAssemblies = AssemblyHandler.Assemblies;
+ AssemblyHandler.Assemblies = [.. _previousAssemblies ?? [], visibleAssembly];
+ }
+
+ if (directoryPrefix != null)
+ {
+ Directory = Path.Combine(Path.GetTempPath(), $"{directoryPrefix}-{Guid.NewGuid():N}");
+ System.IO.Directory.CreateDirectory(Directory);
+ }
+ }
+
+ public SerializationChunkSource Source { get; }
+
+ public SerializationThreadWorker[] Workers { get; }
+
+ /// The scratch directory, or null when none was requested.
+ public string Directory { get; }
+
+ /// Tracks a persistence so it leaves the static registry on dispose.
+ public T Track(T persistence) where T : Persistence
+ {
+ _persistences.Add(persistence);
+ return persistence;
+ }
+
+ public void WakeWorkers()
+ {
+ for (var i = 0; i < Workers.Length; i++)
+ {
+ Workers[i].Wake();
+ }
+ }
+
+ public void SleepWorkers()
+ {
+ for (var i = 0; i < Workers.Length; i++)
+ {
+ Workers[i].Sleep();
+ }
+ }
+
+ /// Mirrors World.PauseSerializationThreads: publish the partial chunk, then wait for the drain.
+ public void FlushAndSleepWorkers()
+ {
+ Source.Flush();
+ SleepWorkers();
+ }
+
+ public void Dispose()
+ {
+ World.SaveStartTime = _previousSaveStart;
+
+ for (var i = 0; i < _persistences.Count; i++)
+ {
+ _persistences[i]?.Unregister();
+ }
+
+ for (var i = 0; i < Workers.Length; i++)
+ {
+ Workers[i].Exit();
+ }
+
+ if (_installedWorkers)
+ {
+ World._threadWorkers = _previousWorkers;
+ }
+
+ if (_swappedAssemblies)
+ {
+ AssemblyHandler.Assemblies = _previousAssemblies;
+ }
+
+ if (Directory != null)
+ {
+ System.IO.Directory.Delete(Directory, true);
+ }
+ }
+}
diff --git a/Projects/Server.Tests/Helpers/SimulatedClockScope.cs b/Projects/Server.Tests/Helpers/SimulatedClockScope.cs
new file mode 100644
index 000000000..cb4b5acfd
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/SimulatedClockScope.cs
@@ -0,0 +1,42 @@
+using System;
+using Server.Items;
+
+namespace Server.Tests;
+
+///
+/// Lets a test move the game clock () and restores it on dispose, so a failed
+/// assert cannot leave every later test running in the future.
+///
+internal sealed class SimulatedClockScope : IDisposable
+{
+ private static readonly TimeSpan _decayTickStep = TimeSpan.FromMilliseconds(256);
+
+ public SimulatedClockScope() => Start = Core._now;
+
+ public DateTime Start { get; }
+
+ public DateTime Now
+ {
+ get => Core._now;
+ set => Core._now = value;
+ }
+
+ ///
+ /// Steps the clock from through , driving the decay
+ /// scheduler each step without the shared timer wheel, and stops early once
+ /// is deleted.
+ ///
+ public void AdvanceDecay(TimeSpan duration, Item item)
+ {
+ var start = Core._now;
+ var deadline = start + duration;
+
+ for (var now = start; now <= deadline && !item.Deleted; now += _decayTickStep)
+ {
+ Core._now = now;
+ DecayScheduler.ProcessTick(now);
+ }
+ }
+
+ public void Dispose() => Core._now = Start;
+}
diff --git a/Projects/Server.Tests/Helpers/TestClientHarness.cs b/Projects/Server.Tests/Helpers/TestClientHarness.cs
new file mode 100644
index 000000000..aa9dde5d0
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/TestClientHarness.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Buffers;
+using Server.Network;
+using Server.Tests.Network;
+
+namespace Server.Tests;
+
+///
+/// A logged-in test client (NetState + Mobile) and byte-pattern probes over what it was sent.
+/// The probes search the raw send buffer, so they only see packets queued since the NetState was created.
+///
+internal static class TestClientHarness
+{
+ public static (NetState, Mobile) CreateClient(Point3D location)
+ {
+ var ns = PacketTestUtilities.CreateTestNetState();
+ ns.Account = new MockAccount();
+
+ var mobile = new Mobile(World.NewMobile);
+ mobile.DefaultMobileInit();
+ ns.Mobile = mobile;
+ mobile.NetState = ns;
+ mobile.MoveToWorld(location, Map.Felucca);
+ return (ns, mobile);
+ }
+
+ public static void DisposeClient(NetState ns, Mobile mobile)
+ {
+ ns.Mobile = null;
+ ns.Dispose();
+ mobile.Delete();
+ }
+
+ public static bool ReceivedRemove(NetState ns, Serial serial) => CountRemoves(ns, serial) > 0;
+
+ public static int CountRemoves(NetState ns, Serial serial)
+ {
+ Span expected = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength];
+ OutgoingEntityPackets.CreateRemoveEntity(expected, serial);
+
+ var span = ns.SendBuffer.GetReadSpan();
+ var count = 0;
+
+ while (true)
+ {
+ var index = span.IndexOf(expected);
+
+ if (index < 0)
+ {
+ break;
+ }
+
+ count++;
+ span = span[(index + expected.Length)..];
+ }
+
+ return count;
+ }
+
+ public static bool ReceivedWorldItem(NetState ns, Item item)
+ {
+ Span expected = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength];
+ var length = OutgoingItemPackets.CreateWorldItem(expected, item);
+ return ns.SendBuffer.GetReadSpan().IndexOf(expected[..length]) >= 0;
+ }
+
+ public static bool ReceivedContentUpdate(NetState ns, Serial serial) => ReceivedPacketForSerial(ns, 0x25, serial);
+
+ public static bool ReceivedEquipUpdate(NetState ns, Serial serial) => ReceivedPacketForSerial(ns, 0x2E, serial);
+
+ public static bool ReceivedOplInfo(NetState ns, Serial serial) => ReceivedPacketForSerial(ns, 0xDC, serial);
+
+ // Each of these packets is fixed-length with the serial immediately after the packet ID.
+ private static bool ReceivedPacketForSerial(NetState ns, byte packetId, Serial serial)
+ {
+ Span expected = stackalloc byte[5];
+ var writer = new SpanWriter(expected);
+ writer.Write(packetId);
+ writer.Write(serial);
+ return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
+ }
+}
diff --git a/Projects/Server.Tests/Helpers/TestMulti.cs b/Projects/Server.Tests/Helpers/TestMulti.cs
new file mode 100644
index 000000000..b7363c3f7
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/TestMulti.cs
@@ -0,0 +1,17 @@
+using Server.Items;
+
+namespace Server.Tests;
+
+///
+/// A multi with caller-supplied components. The test host never loads MultiData, so a multi built
+/// from client data would have no components; without a list this one is empty the same way.
+///
+internal sealed class TestMulti : BaseMulti
+{
+ private readonly MultiComponentList _components;
+
+ public TestMulti(MultiComponentList components = null) : base(0x1) =>
+ _components = components ?? MultiComponentList.Empty;
+
+ public override MultiComponentList Components => _components;
+}
diff --git a/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs b/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs
index 471a5584c..39a73ad7b 100644
--- a/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs
+++ b/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs
@@ -2,13 +2,13 @@ using Xunit;
namespace Server.Tests.Network;
-[Collection("Sequential Server Tests")]
public class ClientVersionTests
{
[Theory]
[InlineData("7.0.33.1", "67.0.33", false, 0)]
[InlineData("7.0.45.65", "67.0.33", false, 1)]
[InlineData("6.0.0.0", "67.0.33", false, -1)]
+ [InlineData("7.0.33.1", "7.0.33.1", true, 0)]
public void TestCCAndECStringCtor(string ccVersion, string ecVersion, bool equal, int comparison)
{
var cc = new ClientVersion(ccVersion);
@@ -22,6 +22,7 @@ public class ClientVersionTests
[InlineData(7, 0, 33, 1, 67, 0, 33, 0, false, 0)]
[InlineData(7, 0, 45, 65, 67, 0, 33, 0, false, 1)]
[InlineData(6, 0, 0, 0, 67, 0, 33, 0, false, -1)]
+ [InlineData(7, 0, 33, 1, 7, 0, 33, 1, true, 0)]
public void TestCCAndECCtor(
int ccMaj, int ccMin, int ccRev, int ccPatch, int ecMaj, int ecMin, int ecRev, int ecPatch,
bool equal, int comparison
diff --git a/Projects/Server.Tests/Tests/Console/ConsoleInputPumpTests.cs b/Projects/Server.Tests/Tests/Console/ConsoleInputPumpTests.cs
index 50d015555..cdf084fb7 100644
--- a/Projects/Server.Tests/Tests/Console/ConsoleInputPumpTests.cs
+++ b/Projects/Server.Tests/Tests/Console/ConsoleInputPumpTests.cs
@@ -70,10 +70,11 @@ public class ConsoleInputPumpTests
cmd => cmd == "ping" ? onPing : null
);
- Task.Run(pump.Run);
+ var run = Task.Run(pump.Run);
Assert.True(invokedWith.Task.Wait(Timeout), "command not dispatched");
Assert.Equal("hello", invokedWith.Task.Result);
+ Assert.True(run.Wait(Timeout), "Run() did not return on EOF");
}
[Fact]
@@ -81,7 +82,7 @@ public class ConsoleInputPumpTests
{
var reader = new BlockingTextReader();
var pump = new ConsoleInputPump(reader, _ => null);
- Task.Run(pump.Run);
+ var run = Task.Run(pump.Run);
// Register the prompt, then wait until it is actually pending before feeding a
// line — otherwise the line could be consumed as a command before registration.
@@ -93,6 +94,7 @@ public class ConsoleInputPumpTests
Assert.Equal("the answer", prompt.Result);
reader.Complete();
+ Assert.True(run.Wait(Timeout), "Run() did not return on EOF");
}
[Fact]
@@ -100,7 +102,7 @@ public class ConsoleInputPumpTests
{
var reader = new BlockingTextReader();
var pump = new ConsoleInputPump(reader, _ => null);
- Task.Run(pump.Run);
+ var run = Task.Run(pump.Run);
var prompt = Task.Run(pump.ReadLine);
WaitUntil(() => pump.HasPendingPrompt, "prompt did not register");
@@ -108,6 +110,7 @@ public class ConsoleInputPumpTests
Assert.True(prompt.Wait(Timeout), "prompt hung on EOF");
Assert.Null(prompt.Result);
+ Assert.True(run.Wait(Timeout), "Run() did not return on EOF");
Assert.False(pump.Running);
}
@@ -116,7 +119,7 @@ public class ConsoleInputPumpTests
{
var reader = new BlockingTextReader();
var pump = new ConsoleInputPump(reader, _ => throw new InvalidOperationException("boom"));
- Task.Run(pump.Run);
+ var run = Task.Run(pump.Run);
// Wait until the reader loop is blocked on its first read, then feed a bad command.
WaitUntil(() => reader.ReadCallCount >= 1, "reader did not start");
@@ -133,6 +136,7 @@ public class ConsoleInputPumpTests
Assert.True(prompt.Wait(Timeout), "prompt hung after throwing lookup");
Assert.Null(prompt.Result);
+ Assert.True(run.Wait(Timeout), "Run() did not return on EOF");
Assert.False(pump.Running);
}
}
diff --git a/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs b/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs
index 7fae1f3bf..cdad2512b 100644
--- a/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs
+++ b/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs
@@ -68,7 +68,7 @@ public class Point3DTests
Assert.Equal(new Point3D(101, 23, 55), Point3D.Parse(" (101,23 ,55) ", null));
// No parenthesis
- Assert.False(Point3D.TryParse("101, 23 55)", null, out p));
+ Assert.False(Point3D.TryParse("101, 23, 55)", null, out p));
Assert.Equal(default, p);
Assert.Throws(() => Point3D.Parse("101, 23, 55)", null));
diff --git a/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs b/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs
index d225b625f..9f1ffdb20 100644
--- a/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs
+++ b/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs
@@ -104,7 +104,7 @@ public class Rectangle3DTests
Assert.False(Rectangle3D.TryParse("(101,,10)+(55, 89 1)", null, out p));
Assert.Equal(default, p);
- Assert.Throws(() => Rectangle3D.Parse("(,23,)+(,89)", null));
+ Assert.Throws(() => Rectangle3D.Parse("(101,,10)+(55, 89 1)", null));
Assert.False(Rectangle3D.TryParse("(,,)+(55,89, 1)", null, out p));
Assert.Equal(default, p);
diff --git a/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs b/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs
index 561ece1f9..a913eb9a9 100644
--- a/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs
+++ b/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs
@@ -90,16 +90,16 @@ public sealed class WorldLocationTests
Assert.Equal(new WorldLocation(101, 23, 55, null), WorldLocation.Parse(" (101,23 ,55) [(-null-)]", null));
// No Brackets
- Assert.False(WorldLocation.TryParse("(101, 23 55) Felucca]", null, out p));
+ Assert.False(WorldLocation.TryParse("(101, 23, 55) Felucca]", null, out p));
Assert.Equal(default, p);
Assert.Throws(() => WorldLocation.Parse("(101, 23, 55) Felucca]", null));
- Assert.False(WorldLocation.TryParse("(101, 23 55) [Felucca", null, out p));
+ Assert.False(WorldLocation.TryParse("(101, 23, 55) [Felucca", null, out p));
Assert.Equal(default, p);
Assert.Throws(() => WorldLocation.Parse("(101, 23, 55) [Felucca", null));
// No parenthesis
- Assert.False(WorldLocation.TryParse("101, 23 55) [Felucca]", null, out p));
+ Assert.False(WorldLocation.TryParse("101, 23, 55) [Felucca]", null, out p));
Assert.Equal(default, p);
Assert.Throws(() => WorldLocation.Parse("101, 23, 55) [Felucca]", null));
diff --git a/Projects/Server.Tests/Tests/Items/ContainerBulkRemovalTests.cs b/Projects/Server.Tests/Tests/Items/ContainerBulkRemovalTests.cs
index c529f8a12..33740ec79 100644
--- a/Projects/Server.Tests/Tests/Items/ContainerBulkRemovalTests.cs
+++ b/Projects/Server.Tests/Tests/Items/ContainerBulkRemovalTests.cs
@@ -6,32 +6,43 @@ namespace Server.Tests;
[Collection("Sequential Server Tests")]
public class ContainerBulkRemovalTests
{
+ // Large enough that a quadratic child removal (a front-to-back IndexOf per child) costs
+ // roughly 2 * 10^8 comparisons and shows up in the test's run time.
+ private const int ChildCount = 20_000;
+
[Fact]
public void DeletingLargeContainer_DeletesAllChildren_AndTotalsStayConsistent()
{
var outer = new Container(0xE75);
- var pack = new Container(0xE75);
- outer.DropItem(pack);
- var children = new Item[100_000];
- for (var i = 0; i < children.Length; i++)
+ try
{
- children[i] = new Item(0x1234);
- pack.DropItem(children[i]);
+ var pack = new Container(0xE75);
+ outer.DropItem(pack);
+
+ var children = new Item[ChildCount];
+ for (var i = 0; i < children.Length; i++)
+ {
+ children[i] = new Item(0x1234);
+ pack.DropItem(children[i]);
+ }
+
+ Assert.Equal(children.Length + 1, outer.TotalItems);
+
+ pack.Delete();
+
+ for (var i = 0; i < children.Length; i++)
+ {
+ Assert.True(children[i].Deleted);
+ }
+
+ Assert.Empty(outer.Items);
+ Assert.Equal(0, outer.TotalItems);
}
-
- Assert.Equal(children.Length + 1, outer.TotalItems);
-
- pack.Delete();
-
- for (var i = 0; i < children.Length; i++)
+ finally
{
- Assert.True(children[i].Deleted);
+ outer.Delete();
}
-
- Assert.Empty(outer.Items);
- Assert.Equal(0, outer.TotalItems);
- outer.Delete();
}
// The tail fast path must not change which entry is removed when the item is elsewhere.
@@ -39,20 +50,27 @@ public class ContainerBulkRemovalTests
public void RemovingFromFrontMiddleAndEnd_RemovesExactlyThatItem()
{
var pack = new Container(0xE75);
- var items = new Item[5];
- for (var i = 0; i < items.Length; i++)
+
+ try
{
- items[i] = new Item(0x1234);
- pack.DropItem(items[i]);
+ var items = new Item[5];
+ for (var i = 0; i < items.Length; i++)
+ {
+ items[i] = new Item(0x1234);
+ pack.DropItem(items[i]);
+ }
+
+ items[0].Delete();
+ items[2].Delete();
+ items[4].Delete();
+
+ Assert.Equal(2, pack.Items.Count);
+ Assert.Same(items[1], pack.Items[0]);
+ Assert.Same(items[3], pack.Items[1]);
+ }
+ finally
+ {
+ pack.Delete();
}
-
- items[0].Delete();
- items[2].Delete();
- items[4].Delete();
-
- Assert.Equal(2, pack.Items.Count);
- Assert.Same(items[1], pack.Items[0]);
- Assert.Same(items[3], pack.Items[1]);
- pack.Delete();
}
}
diff --git a/Projects/Server.Tests/Tests/Items/ContainerChildRemovalTests.cs b/Projects/Server.Tests/Tests/Items/ContainerChildRemovalTests.cs
index 695838185..587753250 100644
--- a/Projects/Server.Tests/Tests/Items/ContainerChildRemovalTests.cs
+++ b/Projects/Server.Tests/Tests/Items/ContainerChildRemovalTests.cs
@@ -1,8 +1,7 @@
-using System;
using Server.Items;
using Server.Network;
-using Server.Tests.Network;
using Xunit;
+using static Server.Tests.TestClientHarness;
namespace Server.Tests;
@@ -44,19 +43,6 @@ public class ContainerChildRemovalTests
private static readonly Point3D _ownerLoc = new(1500, 1500, 0);
- private static (NetState, Mobile) CreateClient(Point3D location)
- {
- var ns = PacketTestUtilities.CreateTestNetState();
- ns.Account = new MockAccount();
-
- var mobile = new Mobile(World.NewMobile);
- mobile.DefaultMobileInit();
- ns.Mobile = mobile;
- mobile.NetState = ns;
- mobile.MoveToWorld(location, Map.Felucca);
- return (ns, mobile);
- }
-
private static Mobile CreateOwnerWith(Container pack)
{
var owner = new Mobile(World.NewMobile);
@@ -68,44 +54,6 @@ public class ContainerChildRemovalTests
return owner;
}
- private static bool ReceivedRemove(NetState ns, Serial serial)
- {
- Span expected = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength];
- OutgoingEntityPackets.CreateRemoveEntity(expected, serial);
- return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
- }
-
- private static int CountRemoves(NetState ns, Serial serial)
- {
- Span expected = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength];
- OutgoingEntityPackets.CreateRemoveEntity(expected, serial);
-
- var span = ns.SendBuffer.GetReadSpan();
- var count = 0;
-
- while (true)
- {
- var index = span.IndexOf(expected);
-
- if (index < 0)
- {
- break;
- }
-
- count++;
- span = span[(index + expected.Length)..];
- }
-
- return count;
- }
-
- private static void DisposeClient(NetState ns, Mobile mobile)
- {
- ns.Mobile = null;
- ns.Dispose();
- mobile.Delete();
- }
-
private static void Cleanup(NetState ns, Mobile client, Mobile owner)
{
DisposeClient(ns, client);
diff --git a/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs b/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs
index 0f97eaa28..85ffd3c84 100644
--- a/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs
+++ b/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs
@@ -5,11 +5,15 @@ using Xunit;
namespace Server.Tests;
[Collection("Sequential Server Tests")]
-public class DecayRegistrationTests
+public class DecayRegistrationTests : IDisposable
{
+ private readonly EntityCleanup _cleanup = new();
+
// The wheel is static and these tests move the clock, so start each from a known schedule.
public DecayRegistrationTests() => DecayScheduler.ResetForTests();
+ public void Dispose() => _cleanup.Dispose();
+
// Stands in for a region that refuses decay, e.g. HouseRegion over a locked down item.
private class RefusesDecayItem : Item
{
@@ -20,61 +24,34 @@ public class DecayRegistrationTests
public override bool OnDecay() => false;
}
- // The item is already dequeued, so a refusal that does not reschedule strands it forever.
- // Drives the scheduler and the game clock without depending on the shared timer wheel.
- private static void AdvanceDecay(DateTime start, TimeSpan duration, Item item)
- {
- var deadline = start + duration;
-
- for (var now = start; now <= deadline && !item.Deleted; now += TimeSpan.FromMilliseconds(256))
- {
- Core._now = now;
- DecayScheduler.ProcessTick(now);
- }
- }
-
[Fact]
public void ItemOnGround_ActuallyDecaysAfterDecayTime()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
- Assert.True(DecayScheduler.IsRegistered(item));
+ Assert.True(DecayScheduler.IsRegistered(item));
- AdvanceDecay(start, item.DecayTime + TimeSpan.FromMinutes(2), item);
+ clock.AdvanceDecay(item.DecayTime + TimeSpan.FromMinutes(2), item);
- Assert.True(item.Deleted, "Item on the ground must decay once DecayTime elapses.");
- }
- finally
- {
- Core._now = start;
- }
+ Assert.True(item.Deleted, "Item on the ground must decay once DecayTime elapses.");
}
[Fact]
public void ItemOnGround_DoesNotDecayBeforeDecayTime()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(102, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(102, 100, 0), Map.Felucca);
- AdvanceDecay(start, item.DecayTime - TimeSpan.FromMinutes(2), item);
+ clock.AdvanceDecay(item.DecayTime - TimeSpan.FromMinutes(2), item);
- Assert.False(item.Deleted, "Item must not decay before DecayTime elapses.");
+ Assert.False(item.Deleted, "Item must not decay before DecayTime elapses.");
- item.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ item.Delete();
}
// The real client path: Mobile.Lift internalizes, then Mobile.Drop calls MoveToWorld.
@@ -84,7 +61,7 @@ public class DecayRegistrationTests
[Fact]
public void DecaySlot_AgreesWithStructures_AcrossLifecycle()
{
- var item = new Item(0x1234);
+ var item = _cleanup.Track(new Item(0x1234));
item.MoveToWorld(new Point3D(103, 100, 0), Map.Felucca);
Assert.NotEqual(DecayScheduler.SlotNone, item.DecaySlot);
@@ -108,10 +85,10 @@ public class DecayRegistrationTests
[Fact]
public void ContainerDestroy_ContentsDroppedToGroundAreTracked()
{
- var pack = new Container(0xE75);
+ var pack = _cleanup.Track(new Container(0xE75));
pack.MoveToWorld(new Point3D(105, 100, 0), Map.Felucca);
- var item = new Item(0x1234);
+ var item = _cleanup.Track(new Item(0x1234));
pack.AddItem(item);
Assert.False(DecayScheduler.IsRegistered(item));
@@ -128,163 +105,127 @@ public class DecayRegistrationTests
[Fact]
public void StaleImmovableItemMadeMovable_GetsAFreshDecayWindow()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(107, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(107, 100, 0), Map.Felucca);
- item.Movable = false;
- Assert.False(DecayScheduler.IsRegistered(item), "A frozen item must not be tracked for decay.");
+ item.Movable = false;
+ Assert.False(DecayScheduler.IsRegistered(item), "A frozen item must not be tracked for decay.");
- Core._now = start + TimeSpan.FromDays(30);
- var flipped = Core._now;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
- item.Movable = true;
+ item.Movable = true;
- Assert.True(DecayScheduler.IsRegistered(item), "An unfrozen item must be tracked for decay.");
+ Assert.True(DecayScheduler.IsRegistered(item), "An unfrozen item must be tracked for decay.");
- AdvanceDecay(flipped, item.DecayTime - TimeSpan.FromMinutes(2), item);
- Assert.False(item.Deleted, "An unfrozen item must get a full decay window, not vanish immediately.");
+ clock.AdvanceDecay(item.DecayTime - TimeSpan.FromMinutes(2), item);
+ Assert.False(item.Deleted, "An unfrozen item must get a full decay window, not vanish immediately.");
- AdvanceDecay(Core._now, TimeSpan.FromMinutes(4), item);
- Assert.True(item.Deleted, "An unfrozen item must still decay once the fresh window elapses.");
- }
- finally
- {
- Core._now = start;
- }
+ clock.AdvanceDecay(TimeSpan.FromMinutes(4), item);
+ Assert.True(item.Deleted, "An unfrozen item must still decay once the fresh window elapses.");
}
// Same transition through the Visible setter: unhiding a long-hidden item.
[Fact]
public void StaleHiddenItemMadeVisible_GetsAFreshDecayWindow()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(109, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(109, 100, 0), Map.Felucca);
- item.Visible = false;
- Assert.False(DecayScheduler.IsRegistered(item), "A hidden item must not be tracked for decay.");
+ item.Visible = false;
+ Assert.False(DecayScheduler.IsRegistered(item), "A hidden item must not be tracked for decay.");
- Core._now = start + TimeSpan.FromDays(30);
- var flipped = Core._now;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
- item.Visible = true;
+ item.Visible = true;
- Assert.True(DecayScheduler.IsRegistered(item), "An unhidden item must be tracked for decay.");
+ Assert.True(DecayScheduler.IsRegistered(item), "An unhidden item must be tracked for decay.");
- AdvanceDecay(flipped, item.DecayTime - TimeSpan.FromMinutes(2), item);
- Assert.False(item.Deleted, "An unhidden item must get a full decay window, not vanish immediately.");
+ clock.AdvanceDecay(item.DecayTime - TimeSpan.FromMinutes(2), item);
+ Assert.False(item.Deleted, "An unhidden item must get a full decay window, not vanish immediately.");
- AdvanceDecay(Core._now, TimeSpan.FromMinutes(4), item);
- Assert.True(item.Deleted, "An unhidden item must still decay once the fresh window elapses.");
- }
- finally
- {
- Core._now = start;
- }
+ clock.AdvanceDecay(TimeSpan.FromMinutes(4), item);
+ Assert.True(item.Deleted, "An unhidden item must still decay once the fresh window elapses.");
}
- // A refusal restarts the countdown without rewriting LastMoved.
+ // A refusal restarts the countdown without rewriting LastMoved. The item is already dequeued
+ // when OnDecay refuses, so a refusal that does not reschedule strands it forever.
[Fact]
public void RefusedDecay_DoesNotRewriteLastMoved()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new RefusesDecayItem();
- item.MoveToWorld(new Point3D(110, 100, 0), Map.Felucca);
- var lastMoved = item.LastMoved;
+ var item = _cleanup.Track(new RefusesDecayItem());
+ item.MoveToWorld(new Point3D(110, 100, 0), Map.Felucca);
+ var lastMoved = item.LastMoved;
- AdvanceDecay(start, item.DecayTime + TimeSpan.FromMinutes(2), item);
+ clock.AdvanceDecay(item.DecayTime + TimeSpan.FromMinutes(2), item);
- Assert.False(item.Deleted, "A refused decay must not delete the item.");
- Assert.True(DecayScheduler.IsRegistered(item), "A refused decay must leave the item tracked.");
- Assert.Equal(lastMoved, item.LastMoved);
+ Assert.False(item.Deleted, "A refused decay must not delete the item.");
+ Assert.True(DecayScheduler.IsRegistered(item), "A refused decay must leave the item tracked.");
+ Assert.Equal(lastMoved, item.LastMoved);
- item.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ item.Delete();
}
// The fresh window must survive a save/load cycle, or a restart mid-window deletes the item.
[Fact]
public void FreshDecayWindow_SurvivesSerialization()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(111, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(111, 100, 0), Map.Felucca);
- item.Movable = false;
- Core._now = start + TimeSpan.FromDays(30);
- item.Movable = true;
+ item.Movable = false;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
+ item.Movable = true;
- var expected = item.ScheduledDecayTime;
+ var expected = item.ScheduledDecayTime;
- var writer = new BufferWriter(new byte[512], true);
- item.Serialize(writer);
+ var writer = new BufferWriter(new byte[512], true);
+ item.Serialize(writer);
- var copy = new Item(item.Serial);
- copy.Deserialize(new BufferReader(writer.Buffer));
+ var copy = new Item(item.Serial);
+ copy.Deserialize(new BufferReader(writer.Buffer));
- // The stamp is stored as a delta, so it ages only by the real time between
- // write and read - milliseconds here, the downtime in production.
- Assert.True(
- (copy.ScheduledDecayTime - expected).Duration() <= TimeSpan.FromSeconds(5),
- "The restarted decay window must survive a save/load cycle."
- );
+ // The stamp is stored as a delta, so it ages only by the real time between
+ // write and read - milliseconds here, the downtime in production.
+ Assert.True(
+ (copy.ScheduledDecayTime - expected).Duration() <= TimeSpan.FromSeconds(5),
+ "The restarted decay window must survive a save/load cycle."
+ );
- item.Delete();
- copy.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ item.Delete();
+ copy.Delete();
}
// A real move supersedes the reset stamp; it must be dropped so the CompactInfo can collapse.
[Fact]
public void MovingAnItem_ClearsASupersededDecayResetStamp()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(112, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(112, 100, 0), Map.Felucca);
- item.Movable = false;
- Core._now = start + TimeSpan.FromDays(30);
- item.Movable = true;
+ item.Movable = false;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
+ item.Movable = true;
- Assert.NotEqual(default, item.DecayResetTime);
+ Assert.NotEqual(default, item.DecayResetTime);
- Core._now += TimeSpan.FromMinutes(1);
- item.MoveToWorld(new Point3D(113, 100, 0), Map.Felucca);
+ clock.Now += TimeSpan.FromMinutes(1);
+ item.MoveToWorld(new Point3D(113, 100, 0), Map.Felucca);
- Assert.Equal(default, item.DecayResetTime);
- Assert.Equal(item.LastMoved + item.DecayTime, item.ScheduledDecayTime);
- Assert.True(DecayScheduler.IsRegistered(item));
+ Assert.Equal(default, item.DecayResetTime);
+ Assert.Equal(item.LastMoved + item.DecayTime, item.ScheduledDecayTime);
+ Assert.True(DecayScheduler.IsRegistered(item));
- item.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ item.Delete();
}
// Losing decay eligibility makes the stamp meaningless; it must be dropped so the
@@ -292,29 +233,22 @@ public class DecayRegistrationTests
[Fact]
public void ItemBecomingIneligible_DropsTheDecayResetStamp()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(115, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(115, 100, 0), Map.Felucca);
- item.Movable = false;
- Core._now = start + TimeSpan.FromDays(30);
- item.Movable = true;
+ item.Movable = false;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
+ item.Movable = true;
- Assert.NotEqual(default, item.DecayResetTime);
+ Assert.NotEqual(default, item.DecayResetTime);
- item.Movable = false;
+ item.Movable = false;
- Assert.Equal(default, item.DecayResetTime);
+ Assert.Equal(default, item.DecayResetTime);
- item.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ item.Delete();
}
// Moving a stamped item into a container programmatically (no drop, no SetLastMoved)
@@ -322,32 +256,25 @@ public class DecayRegistrationTests
[Fact]
public void StampedItemAddedToContainer_DropsTheDecayResetStamp()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var pack = new Container(0xE75);
- pack.MoveToWorld(new Point3D(116, 100, 0), Map.Felucca);
+ var pack = _cleanup.Track(new Container(0xE75));
+ pack.MoveToWorld(new Point3D(116, 100, 0), Map.Felucca);
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(117, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(117, 100, 0), Map.Felucca);
- item.Movable = false;
- Core._now = start + TimeSpan.FromDays(30);
- item.Movable = true;
+ item.Movable = false;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
+ item.Movable = true;
- Assert.NotEqual(default, item.DecayResetTime);
+ Assert.NotEqual(default, item.DecayResetTime);
- pack.AddItem(item);
+ pack.AddItem(item);
- Assert.Equal(default, item.DecayResetTime);
+ Assert.Equal(default, item.DecayResetTime);
- pack.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ pack.Delete();
}
// A raw Map assignment (e.g. a GM changing Map through props) is a move: it must
@@ -355,7 +282,7 @@ public class DecayRegistrationTests
[Fact]
public void ItemMovedToRealMapViaMapSetter_IsRegisteredForDecay()
{
- var item = new Item(0x1234);
+ var item = _cleanup.Track(new Item(0x1234));
Assert.False(DecayScheduler.IsRegistered(item));
item.Map = Map.Felucca;
@@ -371,10 +298,10 @@ public class DecayRegistrationTests
[Fact]
public void PartialLiftOfGroundStack_LeavesRemainderRegisteredForDecay()
{
- var stack = new Item(0x1234) { Stackable = true, Amount = 10 };
+ var stack = _cleanup.Track(new Item(0x1234) { Stackable = true, Amount = 10 });
stack.MoveToWorld(new Point3D(114, 100, 0), Map.Felucca);
- var remainder = Mobile.LiftItemDupe(stack, 3);
+ var remainder = _cleanup.Track(Mobile.LiftItemDupe(stack, 3));
Assert.NotNull(remainder);
Assert.Equal(7, remainder.Amount);
@@ -388,6 +315,4 @@ public class DecayRegistrationTests
stack.Delete();
remainder.Delete();
}
-
- // Dropping into a container must untrack; taking it back out to the ground must re-track.
}
diff --git a/Projects/Server.Tests/Tests/Items/LiftRejectResyncTests.cs b/Projects/Server.Tests/Tests/Items/LiftRejectResyncTests.cs
index 97cffbba9..03b372e14 100644
--- a/Projects/Server.Tests/Tests/Items/LiftRejectResyncTests.cs
+++ b/Projects/Server.Tests/Tests/Items/LiftRejectResyncTests.cs
@@ -1,9 +1,7 @@
-using System;
-using System.Buffers;
using Server.Items;
using Server.Network;
-using Server.Tests.Network;
using Xunit;
+using static Server.Tests.TestClientHarness;
namespace Server.Tests;
@@ -12,51 +10,6 @@ public class LiftRejectResyncTests
{
private static readonly Point3D _baseLoc = new(2200, 2200, 0);
- private static (NetState, Mobile) CreateClient(Point3D location)
- {
- var ns = PacketTestUtilities.CreateTestNetState();
- ns.Account = new MockAccount();
-
- var mobile = new Mobile(World.NewMobile);
- mobile.DefaultMobileInit();
- ns.Mobile = mobile;
- mobile.NetState = ns;
- mobile.MoveToWorld(location, Map.Felucca);
- return (ns, mobile);
- }
-
- private static void DisposeClient(NetState ns, Mobile mobile)
- {
- ns.Mobile = null;
- ns.Dispose();
- mobile.Delete();
- }
-
- private static bool ReceivedContentUpdate(NetState ns, Serial serial)
- {
- Span expected = stackalloc byte[5];
- var writer = new SpanWriter(expected);
- writer.Write((byte)0x25); // ContainerContentUpdate packet ID
- writer.Write(serial);
- return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
- }
-
- private static bool ReceivedWorldItem(NetState ns, Item item)
- {
- Span expected = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength];
- var length = OutgoingItemPackets.CreateWorldItem(expected, item);
- return ns.SendBuffer.GetReadSpan().IndexOf(expected[..length]) >= 0;
- }
-
- private static bool ReceivedEquipUpdate(NetState ns, Serial serial)
- {
- Span expected = stackalloc byte[5];
- var writer = new SpanWriter(expected);
- writer.Write((byte)0x2E); // EquipUpdate packet ID
- writer.Write(serial);
- return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
- }
-
[Fact]
public void PrivateNestedItem_RejectedLift_DoesNotResyncBystander()
{
diff --git a/Projects/Server.Tests/Tests/Items/LockedDownContainerDecayTests.cs b/Projects/Server.Tests/Tests/Items/LockedDownContainerDecayTests.cs
index b8b4fd419..e7d790b7b 100644
--- a/Projects/Server.Tests/Tests/Items/LockedDownContainerDecayTests.cs
+++ b/Projects/Server.Tests/Tests/Items/LockedDownContainerDecayTests.cs
@@ -5,8 +5,10 @@ using Xunit;
namespace Server.Tests;
[Collection("Sequential Server Tests")]
-public class LockedDownContainerDecayTests
+public class LockedDownContainerDecayTests : IDisposable
{
+ private readonly EntityCleanup _cleanup = new();
+
public LockedDownContainerDecayTests()
{
DecayScheduler.ResetForTests();
@@ -19,6 +21,8 @@ public class LockedDownContainerDecayTests
}
}
+ public void Dispose() => _cleanup.Dispose();
+
private class TestChest : Container
{
public TestChest() : base(0xE43)
@@ -35,11 +39,11 @@ public class LockedDownContainerDecayTests
public override bool ContentsDecay => false;
}
- private static Item NewLoot() => new Item(0x1F03) { Movable = true, Visible = true };
+ private Item NewLoot() => _cleanup.Track(new Item(0x1F03) { Movable = true, Visible = true });
- private static TestChest PlaceLockedDownChest(int x)
+ private TestChest PlaceLockedDownChest(int x)
{
- var chest = new TestChest { Movable = false };
+ var chest = _cleanup.Track(new TestChest { Movable = false });
chest.MoveToWorld(new Point3D(x, 100, 0), Map.Felucca);
chest.IsLockedDown = true;
return chest;
@@ -62,7 +66,7 @@ public class LockedDownContainerDecayTests
[Fact]
public void ItemInOrdinaryContainer_IsNotTracked()
{
- var chest = new TestChest();
+ var chest = _cleanup.Track(new TestChest());
chest.MoveToWorld(new Point3D(121, 100, 0), Map.Felucca);
var loot = NewLoot();
@@ -77,7 +81,7 @@ public class LockedDownContainerDecayTests
[Fact]
public void LockingAndReleasingContainer_RegistersAndUnregistersContents()
{
- var chest = new TestChest { Movable = false };
+ var chest = _cleanup.Track(new TestChest { Movable = false });
chest.MoveToWorld(new Point3D(122, 100, 0), Map.Felucca);
var loot = NewLoot();
chest.AddItem(loot);
@@ -102,7 +106,7 @@ public class LockedDownContainerDecayTests
[Fact]
public void ContainerThatKeepsContents_DoesNotTrackThem()
{
- var board = new KeepsContentsChest { Movable = false };
+ var board = _cleanup.Track(new KeepsContentsChest { Movable = false });
board.MoveToWorld(new Point3D(123, 100, 0), Map.Felucca);
board.IsLockedDown = true;
var piece = NewLoot();
@@ -118,7 +122,7 @@ public class LockedDownContainerDecayTests
public void NestedContainerContents_AreNotTracked()
{
var chest = PlaceLockedDownChest(124);
- var bag = new TestChest();
+ var bag = _cleanup.Track(new TestChest());
chest.AddItem(bag);
var loot = NewLoot();
bag.AddItem(loot);
@@ -145,29 +149,17 @@ public class LockedDownContainerDecayTests
[Fact]
public void TrackedContents_DecayOnSchedule()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var chest = PlaceLockedDownChest(126);
- var loot = NewLoot();
- chest.AddItem(loot);
+ var chest = PlaceLockedDownChest(126);
+ var loot = NewLoot();
+ chest.AddItem(loot);
- var deadline = start + loot.DecayTime + TimeSpan.FromMinutes(2);
- for (var now = start; now <= deadline && !loot.Deleted; now += TimeSpan.FromMilliseconds(256))
- {
- Core._now = now;
- DecayScheduler.ProcessTick(now);
- }
+ clock.AdvanceDecay(loot.DecayTime + TimeSpan.FromMinutes(2), loot);
- Assert.True(loot.Deleted, "Contents of a locked-down container must decay.");
- Assert.False(chest.Deleted, "The locked-down container itself must not decay.");
+ Assert.True(loot.Deleted, "Contents of a locked-down container must decay.");
+ Assert.False(chest.Deleted, "The locked-down container itself must not decay.");
- chest.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ chest.Delete();
}
}
diff --git a/Projects/Server.Tests/Tests/Items/PrivateChildPropertiesTests.cs b/Projects/Server.Tests/Tests/Items/PrivateChildPropertiesTests.cs
index 4481b08b2..97b67613f 100644
--- a/Projects/Server.Tests/Tests/Items/PrivateChildPropertiesTests.cs
+++ b/Projects/Server.Tests/Tests/Items/PrivateChildPropertiesTests.cs
@@ -1,15 +1,24 @@
using System;
-using System.Buffers;
using Server.Items;
using Server.Network;
-using Server.Tests.Network;
using Xunit;
+using static Server.Tests.TestClientHarness;
namespace Server.Tests;
[Collection("Sequential Server Tests")]
-public class PrivateChildPropertiesTests
+public class PrivateChildPropertiesTests : IDisposable
{
+ private readonly bool _oplWasEnabled;
+
+ public PrivateChildPropertiesTests()
+ {
+ _oplWasEnabled = ObjectPropertyList.Enabled;
+ ObjectPropertyList.Enabled = true;
+ }
+
+ public void Dispose() => ObjectPropertyList.Enabled = _oplWasEnabled;
+
private class PublicContainer : Container
{
public PublicContainer() : base(0xE75)
@@ -21,35 +30,6 @@ public class PrivateChildPropertiesTests
private static readonly Point3D _ownerLoc = new(1600, 1600, 0);
- private static (NetState, Mobile) CreateClient(Point3D location)
- {
- var ns = PacketTestUtilities.CreateTestNetState();
- ns.Account = new MockAccount();
-
- var mobile = new Mobile(World.NewMobile);
- mobile.DefaultMobileInit();
- ns.Mobile = mobile;
- mobile.NetState = ns;
- mobile.MoveToWorld(location, Map.Felucca);
- return (ns, mobile);
- }
-
- private static void DisposeClient(NetState ns, Mobile mobile)
- {
- ns.Mobile = null;
- ns.Dispose();
- mobile.Delete();
- }
-
- private static bool ReceivedOplInfo(NetState ns, Serial serial)
- {
- Span expected = stackalloc byte[5];
- var writer = new SpanWriter(expected);
- writer.Write((byte)0xDC); // OPL info packet ID
- writer.Write(serial);
- return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
- }
-
// AddItem/MoveToWorld already queue their own Update (+Properties) delta as a side effect of the
// Map setter. Draining it first isolates the properties-only delta the test actually exercises;
// otherwise the leftover Update flag routes through the Update branch and the test would pass or
@@ -68,9 +48,6 @@ public class PrivateChildPropertiesTests
[Fact]
public void PrivateChildInOwnerBackpack_PropertiesOnlyUpdate_ReachesOwnerNotBystander()
{
- var wasEnabled = ObjectPropertyList.Enabled;
- ObjectPropertyList.Enabled = true;
-
var (ownerNs, owner) = CreateClient(_ownerLoc);
var (bystanderNs, bystander) = CreateClient(new Point3D(_ownerLoc.X + 1, _ownerLoc.Y, 0));
@@ -93,16 +70,12 @@ public class PrivateChildPropertiesTests
child.Delete();
DisposeClient(ownerNs, owner);
DisposeClient(bystanderNs, bystander);
- ObjectPropertyList.Enabled = wasEnabled;
}
}
[Fact]
public void PrivateGroundContainer_PropertiesOnlyUpdate_ReachesOpenerNotNonOpener()
{
- var wasEnabled = ObjectPropertyList.Enabled;
- ObjectPropertyList.Enabled = true;
-
var (openerNs, opener) = CreateClient(_ownerLoc);
var (bystanderNs, bystander) = CreateClient(new Point3D(_ownerLoc.X + 2, _ownerLoc.Y, 0));
@@ -126,16 +99,12 @@ public class PrivateChildPropertiesTests
container.Delete();
DisposeClient(openerNs, opener);
DisposeClient(bystanderNs, bystander);
- ObjectPropertyList.Enabled = wasEnabled;
}
}
[Fact]
public void PublicContainerChild_PropertiesOnlyUpdate_StillReachesBystander()
{
- var wasEnabled = ObjectPropertyList.Enabled;
- ObjectPropertyList.Enabled = true;
-
var (bystanderNs, bystander) = CreateClient(_ownerLoc);
var pack = new PublicContainer();
@@ -155,16 +124,12 @@ public class PrivateChildPropertiesTests
{
pack.Delete();
DisposeClient(bystanderNs, bystander);
- ObjectPropertyList.Enabled = wasEnabled;
}
}
[Fact]
public void GroundItem_PropertiesOnlyUpdate_StillReachesBystander()
{
- var wasEnabled = ObjectPropertyList.Enabled;
- ObjectPropertyList.Enabled = true;
-
var (bystanderNs, bystander) = CreateClient(_ownerLoc);
var item = new Item(0x1234);
@@ -181,7 +146,6 @@ public class PrivateChildPropertiesTests
{
item.Delete();
DisposeClient(bystanderNs, bystander);
- ObjectPropertyList.Enabled = wasEnabled;
}
}
}
diff --git a/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs b/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs
index a240a0576..c4c48b41b 100644
--- a/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs
+++ b/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs
@@ -1,18 +1,28 @@
+using System;
using Xunit;
namespace Server.Tests;
[Collection("Sequential Server Tests")]
-public class LocalizationEntryTests
+public class LocalizationEntryTests : IDisposable
{
+ private const int FormatNumber = 500002;
+ private const int ParameterNumber = 500003;
+
+ public void Dispose()
+ {
+ Localization.Remove("enu", FormatNumber);
+ Localization.Remove("enu", ParameterNumber);
+ }
+
[Fact]
public void TestClilocAsParameter()
{
- Localization.Add("enu", 500002, "This tests ~1_NUMBER~ as parameters.");
- Localization.Add("enu", 500003, "clilocs");
+ Localization.Add("enu", FormatNumber, "This tests ~1_NUMBER~ as parameters.");
+ Localization.Add("enu", ParameterNumber, "clilocs");
- string numericFormatter = Localization.Format(500002, "enu", $"{500003:#}");
- string stringParam = Localization.Format(500002, "enu", $"{"#500003"}");
+ string numericFormatter = Localization.Format(FormatNumber, "enu", $"{ParameterNumber:#}");
+ string stringParam = Localization.Format(FormatNumber, "enu", $"{"#500003"}");
Assert.Equal("This tests clilocs as parameters.", numericFormatter);
Assert.Equal("This tests clilocs as parameters.", stringParam);
diff --git a/Projects/Server.Tests/Tests/Maps/CanFitItemTests.cs b/Projects/Server.Tests/Tests/Maps/CanFitItemTests.cs
index 684c77743..5531e0530 100644
--- a/Projects/Server.Tests/Tests/Maps/CanFitItemTests.cs
+++ b/Projects/Server.Tests/Tests/Maps/CanFitItemTests.cs
@@ -33,6 +33,8 @@ public class CanFitItemTests
Assert.False(map.CanFitItem(100, map.Height + 1, 0, 1));
}
+ // Plain walkable land in the real Felucca map. Without map files every cell is the default land
+ // tile at Z 0 with no flags, which these tests treat the same way: a surface with nothing above it.
private const int TestLandX = 1500;
private const int TestLandY = 1600;
@@ -170,23 +172,19 @@ public class CanFitItemTests
Item movableItem = null;
try
{
- // Create a movable surface item
+ // Raised well clear of the land so the movable item's top is the only candidate surface there
+ var itemZ = map.GetAverageZ(x, y) + 20;
+
movableItem = new Item(ServerFixture.SurfaceTileId)
{
Movable = true
};
- movableItem.MoveToWorld(new Point3D(x, y, 10), map);
+ movableItem.MoveToWorld(new Point3D(x, y, itemZ), map);
- var itemTop = 10 + movableItem.ItemData.CalcHeight;
- // Try placing at item top - should fail unless there's land there too
- var avgZ = map.GetAverageZ(x, y);
+ var itemTop = itemZ + movableItem.ItemData.CalcHeight;
+ var result = map.CanFitItem(x, y, itemTop, 1);
- // If avgZ != itemTop, there's no other surface, so it should fail
- if (avgZ != itemTop)
- {
- var result = map.CanFitItem(x, y, itemTop, 1);
- Assert.False(result, "Movable item should not count as valid surface");
- }
+ Assert.False(result, "Movable item should not count as valid surface");
}
finally
{
@@ -248,16 +246,4 @@ public class CanFitItemTests
multi.MoveToWorld(location, map);
return multi;
}
-
- private class TestMulti : BaseMulti
- {
- private readonly MultiComponentList _components;
-
- public TestMulti(MultiComponentList components) : base(0x1)
- {
- _components = components ?? MultiComponentList.Empty;
- }
-
- public override MultiComponentList Components => _components;
- }
}
diff --git a/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTests.cs b/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTests.cs
index c90cce9ea..23bc9d173 100644
--- a/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTests.cs
+++ b/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTests.cs
@@ -29,7 +29,8 @@ public class CanSpawnMobileTests
Assert.False(map.CanSpawnMobile(100, map.Height + 1, -128, 127, false, false, out _));
}
- // Use coordinates near Britain which should be walkable land in both test and real data
+ // Plain walkable land near Britain in the real Felucca map. Without map files every cell is the
+ // default land tile at Z 0 with no flags, which CanSpawnMobile treats the same way: walkable, not water.
private const int TestLandX = 1500;
private const int TestLandY = 1600;
@@ -37,10 +38,9 @@ public class CanSpawnMobileTests
public void CanSpawnMobile_EmptyLocationWithLandReturnsTrue()
{
var map = Map.Felucca;
- map.GetAverageZ(TestLandX, TestLandY, out _, out var landZ, out _);
// Test with full Z range to find land
- var result = map.CanSpawnMobile(TestLandX, TestLandY, -128, 127, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(TestLandX, TestLandY, -128, 127, false, false, out _);
// Should find a valid spawn point (land surface)
Assert.True(result, $"Expected to find valid spawn point on land at ({TestLandX}, {TestLandY})");
@@ -53,7 +53,7 @@ public class CanSpawnMobileTests
map.GetAverageZ(TestLandX, TestLandY, out _, out var avgZ, out _);
// Use a Z range that excludes the land surface (50+ units above land)
- var result = map.CanSpawnMobile(TestLandX, TestLandY, avgZ + 50, avgZ + 100, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(TestLandX, TestLandY, avgZ + 50, avgZ + 100, false, false, out _);
Assert.False(result, $"Expected no spawn point above land at Z={avgZ}");
}
@@ -63,7 +63,7 @@ public class CanSpawnMobileTests
{
var map = Map.Felucca;
// cantWalk=true means the mob can only swim, so land shouldn't be valid
- var result = map.CanSpawnMobile(TestLandX, TestLandY, -128, 127, false, true, out var spawnZ);
+ var result = map.CanSpawnMobile(TestLandX, TestLandY, -128, 127, false, true, out _);
Assert.False(result, "cantWalk=true should not find land as valid surface");
}
@@ -83,7 +83,7 @@ public class CanSpawnMobileTests
blockingMobile.MoveToWorld(new Point3D(TestLandX, TestLandY, landZ), map);
// The mobile should block spawning at its location and Z
- var result = map.CanSpawnMobile(TestLandX, TestLandY, landZ - 16, landZ + 16, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(TestLandX, TestLandY, landZ - 16, landZ + 16, false, false, out _);
// Land surface is blocked by mobile at the same Z
Assert.False(result, "Mobile should block spawn at same location");
@@ -115,7 +115,7 @@ public class CanSpawnMobileTests
gm.MoveToWorld(new Point3D(x, y, landZ), map);
// Hidden GM should not block spawning
- var result = map.CanSpawnMobile(x, y, landZ - 16, landZ + 16, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, landZ - 16, landZ + 16, false, false, out _);
Assert.True(result, "Hidden GM should not block spawn");
}
@@ -142,7 +142,7 @@ public class CanSpawnMobileTests
blockingMobile.MoveToWorld(new Point3D(x, y, landZ + 50), map);
// Land should still be valid (mobile above doesn't block ground level)
- var result = map.CanSpawnMobile(x, y, landZ - 16, landZ + 16, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, landZ - 16, landZ + 16, false, false, out _);
Assert.True(result, "Mobile at different Z should not block");
}
diff --git a/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTileDataTests.cs b/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTileDataTests.cs
index ea12926f1..38d757463 100644
--- a/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTileDataTests.cs
+++ b/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTileDataTests.cs
@@ -11,13 +11,6 @@ namespace Server.Tests.Maps;
[Collection("Sequential Server Tests")]
public class CanSpawnMobileTileDataTests
{
- private readonly ServerFixture _fixture;
-
- public CanSpawnMobileTileDataTests(ServerFixture fixture)
- {
- _fixture = fixture;
- }
-
private void SkipIfNoTileData()
{
Skip.If(!ServerFixture.TileDataLoaded, "TileData not loaded - client files required");
@@ -96,7 +89,7 @@ public class CanSpawnMobileTileDataTests
multi = CreateFloorWithCeilingMulti(map, new Point3D(x, y, 0), floorZ: 20, ceilingZ: 28);
// The low ceiling should block spawning (need 16 units clearance for mobiles)
- var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out _);
Assert.False(result);
}
@@ -121,7 +114,7 @@ public class CanSpawnMobileTileDataTests
// Create a multi with floor at Z=20 and ceiling at Z=50 (30 units clearance)
multi = CreateFloorWithCeilingMulti(map, new Point3D(x, y, 0), floorZ: 20, ceilingZ: 50);
- var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out _);
Assert.True(result);
}
@@ -147,7 +140,7 @@ public class CanSpawnMobileTileDataTests
multi = CreateFloorWithCeilingMulti(map, new Point3D(x, y, 0), floorZ: 20, ceilingZ: 100);
// The blocker at Z=100 shouldn't affect spawning in range 15-35
- var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out _);
Assert.True(result);
}
@@ -173,7 +166,7 @@ public class CanSpawnMobileTileDataTests
multi = CreateWaterMulti(map, new Point3D(x, y, 0), waterZ: 0);
// canSwim=true should find the water surface
- var result = map.CanSpawnMobile(x, y, -10, 10, true, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, -10, 10, true, false, out _);
Assert.True(result);
}
@@ -202,7 +195,7 @@ public class CanSpawnMobileTileDataTests
};
surfaceItem.MoveToWorld(new Point3D(x, y, 10), map);
- var result = map.CanSpawnMobile(x, y, 5, 30, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, 5, 30, false, false, out _);
// Should find either land at Z=0 or the item surface, whichever is lowest
Assert.True(result);
@@ -274,16 +267,4 @@ public class CanSpawnMobileTileDataTests
new MultiTileEntry(ServerFixture.WetTileId, 0, 0, (short)waterZ, TileFlag.Wet)
]);
}
-
- private class TestMulti : BaseMulti
- {
- private readonly MultiComponentList _components;
-
- public TestMulti(MultiComponentList components) : base(0x1)
- {
- _components = components ?? MultiComponentList.Empty;
- }
-
- public override MultiComponentList Components => _components;
- }
}
diff --git a/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs
index c5ed6c517..718fadefc 100644
--- a/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs
+++ b/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs
@@ -313,7 +313,7 @@ public class MultiEnumeratorTests
var map = Map.Felucca;
// Components reach two tiles either side, so at x = 1024 the multi spans sectors 63 and 64
- var spanning = new SpanningMulti();
+ var spanning = new TestMulti(SpanningComponents);
try
{
spanning.MoveToWorld(new Point3D(1024, 1000, 0), map);
@@ -351,29 +351,11 @@ public class MultiEnumeratorTests
}
}
- // Test implementation of BaseMulti
- private class TestMulti : BaseMulti
- {
- public TestMulti() : base(0x1)
- {
- }
- }
-
- private class SpanningMulti : BaseMulti
- {
- // Entries after the first need non-zero flags or MultiComponentList drops them
- private static readonly MultiComponentList _components = new(
- [
- new MultiTileEntry(0x1, -2, 0, 0, TileFlag.Background),
- new MultiTileEntry(0x1, 2, 0, 0, TileFlag.Background)
- ]
- );
-
- public SpanningMulti() : base(0x1)
- {
- }
-
- public override MultiComponentList Components => _components;
- }
+ // Entries after the first need non-zero flags or MultiComponentList drops them
+ private static readonly MultiComponentList SpanningComponents = new(
+ [
+ new MultiTileEntry(0x1, -2, 0, 0, TileFlag.Background),
+ new MultiTileEntry(0x1, 2, 0, 0, TileFlag.Background)
+ ]
+ );
}
-
diff --git a/Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs
index 07b8e37d3..9c6e01a36 100644
--- a/Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs
+++ b/Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs
@@ -13,21 +13,17 @@ public class StaticTileEnumeratorTests
var map = Map.Felucca;
var location = new Point2D(200, 200);
+ // Real map files may have statics here; without them there are none. Either way the multi adds nothing.
+ var mapStatics = CollectTiles(map, location, includeStatics: true, includeMultis: false);
+
TestMulti multi = null;
try
{
- // Create a multi at the location
multi = CreateMultiWithComponents(map, new Point3D(200, 200, 0));
- // Get tiles with statics only (no multis)
- var tiles = new List();
- foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: true, includeMultis: false))
- {
- tiles.Add(tile);
- }
+ var tiles = CollectTiles(map, location, includeStatics: true, includeMultis: false);
- // Should not include multi tiles
- Assert.Empty(tiles);
+ Assert.Equal(mapStatics, tiles);
}
finally
{
@@ -69,21 +65,20 @@ public class StaticTileEnumeratorTests
var map = Map.Felucca;
var location = new Point2D(400, 400);
+ var mapStatics = CollectTiles(map, location, includeStatics: true, includeMultis: false);
+
TestMulti multi = null;
try
{
- // Create a multi at the location
multi = CreateMultiWithComponents(map, new Point3D(400, 400, 0));
- // Get all tiles (statics and multis)
- var tiles = new List();
- foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: true, includeMultis: true))
- {
- tiles.Add(tile);
- }
+ var multiTiles = CollectTiles(map, location, includeStatics: false, includeMultis: true);
+ var tiles = CollectTiles(map, location, includeStatics: true, includeMultis: true);
- // Should include multi tiles (statics would be empty without map files)
- Assert.NotEmpty(tiles);
+ Assert.NotEmpty(multiTiles);
+ Assert.Equal(mapStatics.Count + multiTiles.Count, tiles.Count);
+ Assert.All(mapStatics, tile => Assert.Contains(tile, tiles));
+ Assert.All(multiTiles, tile => Assert.Contains(tile, tiles));
}
finally
{
@@ -257,10 +252,10 @@ public class StaticTileEnumeratorTests
{
// Outer's bounds cover the probe cell but it has no tile there; placing it first puts it
// ahead of inner in the sector's multi list
- outer = new TestMulti(World.NewItem, GappedComponents);
+ outer = new TestMulti(GappedComponents);
outer.MoveToWorld(new Point3D(1100, 1100, 0), map);
- inner = new TestMulti(World.NewItem, SingleTileComponents);
+ inner = new TestMulti(SingleTileComponents);
inner.MoveToWorld(new Point3D(location.X, location.Y, 20), map);
var tiles = new List();
@@ -294,28 +289,28 @@ public class StaticTileEnumeratorTests
]
);
+ private static readonly MultiComponentList DefaultComponents = new(
+ [
+ new MultiTileEntry(0x1, 0, 0, 0, 0x0),
+ new MultiTileEntry(0x2, 1, 0, 0, 0x0)
+ ]
+ );
+
+ private static List CollectTiles(Map map, Point2D location, bool includeStatics, bool includeMultis)
+ {
+ var tiles = new List();
+ foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics, includeMultis))
+ {
+ tiles.Add(tile);
+ }
+
+ return tiles;
+ }
+
private static TestMulti CreateMultiWithComponents(Map map, Point3D location)
{
- var multi = new TestMulti(World.NewItem);
+ var multi = new TestMulti(DefaultComponents);
multi.MoveToWorld(location, map);
return multi;
}
-
- private class TestMulti : BaseMulti
- {
- private readonly MultiComponentList _components;
-
- public TestMulti(Serial serial, MultiComponentList components = null) : base(serial) =>
- _components = components ?? DefaultComponents;
-
- public override MultiComponentList Components => _components;
-
- private static readonly MultiComponentList DefaultComponents = new(
- [
- new MultiTileEntry(0x1, 0, 0, 0, 0x0),
- new MultiTileEntry(0x2, 1, 0, 0, 0x0)
- ]
- );
- }
}
-
diff --git a/Projects/Server.Tests/Tests/Network/Bans/BanChannelTests.cs b/Projects/Server.Tests/Tests/Network/Bans/BanChannelTests.cs
index 5eb1895d8..931947a55 100644
--- a/Projects/Server.Tests/Tests/Network/Bans/BanChannelTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Bans/BanChannelTests.cs
@@ -7,8 +7,18 @@ using Xunit;
namespace Server.Tests.Network.Bans;
-public class BanChannelTests
+[Collection("Sequential Server Tests")]
+public class BanChannelTests : IDisposable
{
+ private readonly IBanReporter[] _previousReporters = [..BanChannel.Reporters];
+ private readonly Func _previousIsExempt = BanChannel.IsExempt;
+
+ public void Dispose()
+ {
+ BanChannel.ConfigureForTesting(_previousReporters);
+ BanChannel.IsExempt = _previousIsExempt;
+ }
+
private sealed class FakeReporter : IBanReporter
{
public readonly List<(IPAddress ip, TimeSpan ttl, string reason)> Reports = [];
@@ -69,18 +79,11 @@ public class BanChannelTests
var exempt = IPAddress.Parse("203.0.113.7");
BanChannel.IsExempt = (ip, _) => ip.Equals(exempt);
- try
- {
- BanChannel.Report(exempt, TimeSpan.FromHours(1), "rate-limit");
- Assert.Empty(reporter.Reports); // escalation suppressed; the local gate already acted
+ BanChannel.Report(exempt, TimeSpan.FromHours(1), "rate-limit");
+ Assert.Empty(reporter.Reports); // escalation suppressed; the local gate already acted
- BanChannel.Report(IPAddress.Parse("203.0.113.8"), TimeSpan.FromHours(1), "rate-limit");
- Assert.Single(reporter.Reports); // everyone else is still contributed
- }
- finally
- {
- BanChannel.IsExempt = null;
- }
+ BanChannel.Report(IPAddress.Parse("203.0.113.8"), TimeSpan.FromHours(1), "rate-limit");
+ Assert.Single(reporter.Reports); // everyone else is still contributed
}
[Fact]
diff --git a/Projects/Server.Tests/Tests/Network/MovementThrottleLatencyTests.cs b/Projects/Server.Tests/Tests/Network/MovementThrottleLatencyTests.cs
index 8644993ab..4e9c82ff5 100644
--- a/Projects/Server.Tests/Tests/Network/MovementThrottleLatencyTests.cs
+++ b/Projects/Server.Tests/Tests/Network/MovementThrottleLatencyTests.cs
@@ -4,89 +4,20 @@ using Xunit;
namespace Server.Tests.Network;
///
-/// Manual tests for MovementThrottle latency scenarios.
-/// These tests simulate specific timing scenarios and should NOT run in CI/CD.
-///
-/// To run these tests manually:
-/// dotnet test --filter "Category=Manual"
-///
-/// To exclude from CI/CD, add to your test command:
-/// dotnet test --filter "Category!=Manual"
+/// MovementThrottle burst forgiveness against a full, wrapped movement history.
///
[Collection("Sequential Server Tests")]
-[Trait("Category", "Manual")]
public class MovementThrottleLatencyTests
{
- ///
- /// Sets the simulated tick count for testing.
- ///
- private static void SetTickCount(long ticks)
- {
- Core._tickCount = ticks;
- }
-
- ///
- /// Simulates recording movements at specific intervals.
- ///
- private static void SimulateMovements(NetState ns, params (int delayMs, int costMs)[] movements)
- {
- ns._movementHistory ??= new NetState.MovementRecord[20];
- ns._movementHistoryIndex = 0;
- ns._movementHistoryFull = false;
- ns._lastMovementRecordTime = 0;
-
- var currentTime = 0L;
-
- foreach (var (delayMs, costMs) in movements)
- {
- currentTime += delayMs;
- SetTickCount(currentTime);
-
- if (ns._lastMovementRecordTime > 0)
- {
- var interval = (int)(currentTime - ns._lastMovementRecordTime);
-
- // Skip chain-breaking gaps (> 2000ms) like the real code does
- if (interval > 2000)
- {
- ns._lastGapDuration = interval;
- ns._lastMovementRecordTime = currentTime;
- continue;
- }
-
- ns._movementHistory[ns._movementHistoryIndex] = new NetState.MovementRecord
- {
- Interval = (short)interval,
- TargetSpeed = (ushort)costMs,
- QueueDepth = 0,
- Flags = 0
- };
-
- ns._movementHistoryIndex++;
- if (ns._movementHistoryIndex >= 20)
- {
- ns._movementHistoryIndex = 0;
- ns._movementHistoryFull = true;
- }
- }
-
- ns._lastMovementRecordTime = currentTime;
- }
- }
-
- ///
- /// Scenario: Verifies that burst forgiveness triggers correctly and clears gap duration.
- ///
[Fact]
- public void MultipleLagSpikes_NoAccumulatedSuspicion()
+ public void AnalyzeMovement_BurstAfterGap_WrappedFullHistory_ForgivesAndClearsGap()
{
var ns = PacketTestUtilities.CreateTestNetState();
- SetTickCount(0);
- // Set up history buffer with mostly normal movements and a burst at the end
+ // The production recorder is private and only reachable through a full movement request, so
+ // the history is laid out directly: 17 normal steps, then a 3-step burst ending at the wrap
ns._movementHistory = new NetState.MovementRecord[20];
- // Fill with 17 normal movements (100ms intervals)
for (var i = 0; i < 17; i++)
{
ns._movementHistory[i] = new NetState.MovementRecord
@@ -98,7 +29,6 @@ public class MovementThrottleLatencyTests
};
}
- // Add 3 burst movements at the end (10ms intervals = burst)
for (var i = 17; i < 20; i++)
{
ns._movementHistory[i] = new NetState.MovementRecord
@@ -110,26 +40,19 @@ public class MovementThrottleLatencyTests
};
}
- ns._movementHistoryIndex = 0; // Next write position (wrapped)
- ns._movementHistoryFull = true; // Buffer is full
+ ns._movementHistoryIndex = 0;
+ ns._movementHistoryFull = true;
- // Set gap duration (simulating a lag spike before the burst)
+ // A lag spike longer than the chain gap preceded the burst
ns._lastGapDuration = 2500;
- // Analyze - burst forgiveness should trigger
- var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var samples, out var confidence);
+ var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out _, out var confidence);
- // Key assertions:
- // 1. Gap duration should be cleared (burst forgiveness triggered)
Assert.Equal(0, ns._lastGapDuration);
-
- // 2. Confidence should be reduced by 0.3x multiplier
Assert.True(
confidence < 0.5f,
$"Burst after gap should have low confidence. Confidence={confidence}, Rate={rate}"
);
-
- // 3. Should NOT be Definite despite the burst (forgiveness applied)
Assert.True(
verdict != MovementThrottle.DetectionVerdict.Definite,
$"Should not be Definite after burst forgiveness. Verdict={verdict}"
diff --git a/Projects/Server.Tests/Tests/Network/NetStateDisconnectTests.cs b/Projects/Server.Tests/Tests/Network/NetStateDisconnectTests.cs
index d1f273db3..fafb15d8a 100644
--- a/Projects/Server.Tests/Tests/Network/NetStateDisconnectTests.cs
+++ b/Projects/Server.Tests/Tests/Network/NetStateDisconnectTests.cs
@@ -1,6 +1,4 @@
using System;
-using System.Diagnostics;
-using System.Net.Sockets;
using Server.Network;
using Xunit;
@@ -96,35 +94,10 @@ public class NetStateDisconnectTests
Assert.True(ns._socket.DisconnectPending);
- var received = new byte[payload.Length];
- var total = 0;
- var deadline = Stopwatch.StartNew();
-
- while (total < payload.Length && deadline.ElapsedMilliseconds < 5000)
- {
- NetState.Slice();
- if (client.Poll(1000, SelectMode.SelectRead))
- {
- var read = client.Receive(received, total, payload.Length - total, SocketFlags.None);
- Assert.NotEqual(0, read);
- total += read;
- }
- }
-
- Assert.Equal(payload, received);
+ Assert.Equal(payload, NetTestHelpers.ReadAll(client, payload.Length, 5000));
// FIN follows once the send completes
- var eof = -1;
- while (eof != 0 && deadline.ElapsedMilliseconds < 5000)
- {
- NetState.Slice();
- if (client.Poll(1000, SelectMode.SelectRead))
- {
- eof = client.Receive(received);
- }
- }
-
- Assert.Equal(0, eof);
+ NetTestHelpers.WaitForEof(client, 5000);
}
finally
{
@@ -156,35 +129,8 @@ public class NetStateDisconnectTests
Assert.True(ns._socket.DisconnectPending);
- var received = new byte[payload.Length];
- var total = 0;
- var deadline = Stopwatch.StartNew();
-
- while (total < payload.Length && deadline.ElapsedMilliseconds < 10000)
- {
- NetState.Slice();
- if (client.Poll(1000, SelectMode.SelectRead))
- {
- var read = client.Receive(received, total, payload.Length - total, SocketFlags.None);
- Assert.NotEqual(0, read);
- total += read;
- }
- }
-
- Assert.Equal(payload.Length, total);
- Assert.Equal(payload, received);
-
- var eof = -1;
- while (eof != 0 && deadline.ElapsedMilliseconds < 10000)
- {
- NetState.Slice();
- if (client.Poll(1000, SelectMode.SelectRead))
- {
- eof = client.Receive(received);
- }
- }
-
- Assert.Equal(0, eof);
+ Assert.Equal(payload, NetTestHelpers.ReadAll(client, payload.Length));
+ NetTestHelpers.WaitForEof(client);
}
finally
{
diff --git a/Projects/Server.Tests/Tests/Network/NetStateRecvGuardTests.cs b/Projects/Server.Tests/Tests/Network/NetStateRecvGuardTests.cs
index 548406ec4..8d596d31b 100644
--- a/Projects/Server.Tests/Tests/Network/NetStateRecvGuardTests.cs
+++ b/Projects/Server.Tests/Tests/Network/NetStateRecvGuardTests.cs
@@ -1,8 +1,4 @@
-using System;
using System.Buffers;
-using System.Diagnostics;
-using System.Network;
-using System.Threading;
using Server.Network;
using Xunit;
@@ -12,71 +8,33 @@ namespace Server.Tests.Network;
public class NetStateRecvGuardTests
{
private const byte TestPacketId = 0xB1;
+ private const byte AttachAccountPacketId = 0x71;
- private static unsafe void RegisterVariableLength()
- {
- if (IncomingPackets.GetHandler(TestPacketId) == null)
- {
- IncomingPackets.Register(TestPacketId, 0, false, &NoOp);
- }
- }
+ private static unsafe NetTestHelpers.HandlerRegistration RegisterVariableLength() =>
+ NetTestHelpers.RegisterHandler(TestPacketId, 0, &NoOp);
private static void NoOp(NetState state, SpanReader reader)
{
}
- // Not 0x73: NetStateSendBufferTests registers its own no-op there first-come-wins, since the
- // handler table is process-global, and this test needs its own handler's side effect to fire.
- private const byte TestPingPacketId = 0x72;
-
- private static byte _lastPing;
-
- private static unsafe void RegisterNoOpPing()
- {
- if (IncomingPackets.GetHandler(TestPingPacketId) == null)
- {
- IncomingPackets.Register(TestPingPacketId, 2, false, &RecordPing);
- }
- }
-
- private static void RecordPing(NetState state, SpanReader reader) => _lastPing = reader.ReadByte();
-
- private const byte AttachAccountPacketId = 0x71;
-
- private static unsafe void RegisterAttachAccount()
- {
- if (IncomingPackets.GetHandler(AttachAccountPacketId) == null)
- {
- IncomingPackets.Register(AttachAccountPacketId, 3, false, &AttachAccount);
- }
- }
+ private static unsafe NetTestHelpers.HandlerRegistration RegisterAttachAccount() =>
+ NetTestHelpers.RegisterHandler(AttachAccountPacketId, 3, &AttachAccount);
// Stands in for the 0x91 handler: the account attaches mid-parse, so the promotion is pending
// with a receive armed when the next packet in the same completion reaches the guard
private static void AttachAccount(NetState state, SpanReader reader) => state.Account = new MockAccount();
- private static void SliceUntil(Func done)
- {
- var deadline = Stopwatch.StartNew();
- while (!done() && deadline.ElapsedMilliseconds < 5000)
- {
- NetState.Slice();
- Thread.Sleep(5);
- }
-
- Assert.True(done());
- }
-
[SkippableFact]
public void HandleReceive_PacketLongerThanTheRecvBuffer_IsAnError()
{
- RegisterVariableLength();
+ // A 16-bit length cannot exceed the 64 KiB base buffer, so the guard is only reachable
+ // through a pre-auth buffer
+ NetTestHelpers.RequirePreAuthBuffers();
+ using var handler = RegisterVariableLength();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
- // A 16-bit length cannot exceed a 64 KiB buffer, so the guard is unreachable there and
- // this only ever hit the `< 3` check on a buffer that size.
- Skip.If(ns._socket.RecvBuffer.PhysicalSize > ushort.MaxValue);
+ Assert.True(ns._socket.RecvBuffer.PhysicalSize <= ushort.MaxValue);
ns._protocolState = NetState.ProtocolState.GameServer_LoggedIn;
@@ -84,7 +42,7 @@ public class NetStateRecvGuardTests
// park the connection with nothing to arm a recv into
var declared = ns._socket.RecvBuffer.PhysicalSize; // one more than the buffer can ever hold
client.Send(new byte[] { TestPacketId, (byte)(declared >> 8), (byte)declared });
- SliceUntil(() => ns._protocolState == NetState.ProtocolState.Error);
+ NetTestHelpers.SliceUntil(() => ns._protocolState == NetState.ProtocolState.Error);
Assert.Contains("bad state", ns._disconnectReason);
}
@@ -98,7 +56,7 @@ public class NetStateRecvGuardTests
[Fact]
public void HandleReceive_PacketThatFits_WaitsForTheRest()
{
- RegisterVariableLength();
+ using var handler = RegisterVariableLength();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -106,7 +64,7 @@ public class NetStateRecvGuardTests
var declared = ns._socket.RecvBuffer.PhysicalSize / 2;
client.Send(new byte[] { TestPacketId, (byte)(declared >> 8), (byte)declared });
- SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 3);
+ NetTestHelpers.SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 3);
Assert.Equal(NetState.ProtocolState.GameServer_LoggedIn, ns._protocolState);
Assert.True(ns.Running);
@@ -122,8 +80,8 @@ public class NetStateRecvGuardTests
[SkippableFact]
public void HandleReceive_OversizePacket_WithAnAccount_RetriesPromotionInsteadOfErroring()
{
- Skip.If(NetState.InitialRecvBufferSize == 0);
- RegisterVariableLength();
+ NetTestHelpers.RequirePreAuthBuffers();
+ using var handler = RegisterVariableLength();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -135,7 +93,7 @@ public class NetStateRecvGuardTests
var declared = NetState.InitialRecvBufferSize; // as much as the initial buffer can ever hold
client.Send(new byte[] { TestPacketId, (byte)(declared >> 8), (byte)declared });
- SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 3);
+ NetTestHelpers.SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 3);
Assert.Equal(NetState.ProtocolState.GameServer_LoggedIn, ns._protocolState);
Assert.True(ns.Running);
@@ -143,7 +101,7 @@ public class NetStateRecvGuardTests
// The deferred promotion applies at this next completion; the header is then delivered
// whole to the 0xB1 no-op handler
client.Send(new byte[declared - 3]);
- SliceUntil(
+ NetTestHelpers.SliceUntil(
() => ns._socket.RecvBuffer.ReadableBytes == 0 &&
ns._socket.RecvBuffer.PhysicalSize == NetState.RecvBufferSize
);
@@ -161,8 +119,8 @@ public class NetStateRecvGuardTests
[SkippableFact]
public void HandleReceive_AfterABurstFillsTheInitialBuffer_KeepsReceiving()
{
- Skip.If(NetState.InitialRecvBufferSize == 0);
- RegisterNoOpPing();
+ NetTestHelpers.RequirePreAuthBuffers();
+ using var ping = NetTestHelpers.RegisterPing();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -174,17 +132,17 @@ public class NetStateRecvGuardTests
var pings = new byte[capacity + 1];
for (var i = 0; i < pings.Length; i += 2)
{
- pings[i] = TestPingPacketId;
+ pings[i] = NetTestHelpers.PingPacketId;
pings[i + 1] = (byte)(i / 2);
}
client.Send(pings);
- SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 0 && ns._receivedData);
+ NetTestHelpers.SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 0 && ns._receivedData);
Assert.True(ns.Running);
// Without a re-arm this ping never arrives
- client.Send(new byte[] { TestPingPacketId, 0xEE });
- SliceUntil(() => _lastPing == 0xEE);
+ client.Send(new byte[] { NetTestHelpers.PingPacketId, 0xEE });
+ NetTestHelpers.SliceUntil(() => NetTestHelpers.LastPing == 0xEE);
Assert.True(ns.Running);
}
finally
@@ -197,9 +155,9 @@ public class NetStateRecvGuardTests
[SkippableFact]
public void HandleReceive_AccountAttachedMidParse_ThenOversizeHeader_WaitsForThePendingPromotion()
{
- Skip.If(NetState.InitialRecvBufferSize == 0);
- RegisterAttachAccount();
- RegisterVariableLength();
+ NetTestHelpers.RequirePreAuthBuffers();
+ using var attach = RegisterAttachAccount();
+ using var handler = RegisterVariableLength();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -212,7 +170,7 @@ public class NetStateRecvGuardTests
// header then hits the guard after the promotion already applied and simply waits on the
// 64 KiB buffer, so the test cannot flake, it just exercises the weaker path on that run.
client.Send(new byte[] { AttachAccountPacketId, 0x00, 0x00, TestPacketId, (byte)(declared >> 8), (byte)declared });
- SliceUntil(() => ns.Account != null);
+ NetTestHelpers.SliceUntil(() => ns.Account != null);
Assert.True(ns.Running);
Assert.Equal(NetState.ProtocolState.GameServer_LoggedIn, ns._protocolState);
@@ -220,7 +178,7 @@ public class NetStateRecvGuardTests
// The next completion applies the promotion and the whole packet is delivered
client.Send(new byte[declared - 3]);
- SliceUntil(
+ NetTestHelpers.SliceUntil(
() => ns._socket.RecvBuffer.ReadableBytes == 0 &&
ns._socket.RecvBuffer.PhysicalSize == NetState.RecvBufferSize
);
diff --git a/Projects/Server.Tests/Tests/Network/NetStateSendBufferTests.cs b/Projects/Server.Tests/Tests/Network/NetStateSendBufferTests.cs
index 6f5a22d04..c92260070 100644
--- a/Projects/Server.Tests/Tests/Network/NetStateSendBufferTests.cs
+++ b/Projects/Server.Tests/Tests/Network/NetStateSendBufferTests.cs
@@ -1,10 +1,8 @@
using System;
using System.Buffers;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Net.Sockets;
using System.Network;
-using System.Threading;
using Server.Network;
using Xunit;
@@ -22,42 +20,11 @@ public class NetStateSendBufferTests
return ns;
}
- private static byte[] Pattern(int length, int seed)
- {
- var data = new byte[length];
- new System.Random(seed).NextBytes(data);
- return data;
- }
-
- private static unsafe void RegisterNoOpPing()
- {
- if (IncomingPackets.GetHandler(0x73) == null)
- {
- IncomingPackets.Register(0x73, 2, false, &NoOp);
- }
- }
-
- private static void NoOp(NetState state, SpanReader reader)
- {
- }
-
- private static void SliceUntil(Func done)
- {
- var deadline = Stopwatch.StartNew();
- while (!done() && deadline.ElapsedMilliseconds < 5000)
- {
- NetState.Slice();
- Thread.Sleep(5);
- }
-
- Assert.True(done());
- }
-
[SkippableFact]
public void Account_InTheGameServerState_PromotesBothBuffers()
{
- Skip.If(NetState.InitialSendBufferSize == 0);
- RegisterNoOpPing();
+ NetTestHelpers.RequirePreAuthBuffers();
+ using var ping = NetTestHelpers.RegisterPing();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -73,8 +40,8 @@ public class NetStateSendBufferTests
// Recv promotes at the next completion
ns._protocolState = NetState.ProtocolState.GameServer_LoggedIn;
- client.Send(new byte[] { 0x73, 0x01 });
- SliceUntil(() => ns._socket.RecvBuffer.PhysicalSize == NetState.RecvBufferSize);
+ client.Send(new byte[] { NetTestHelpers.PingPacketId, 0x01 });
+ NetTestHelpers.SliceUntil(() => ns._socket.RecvBuffer.PhysicalSize == NetState.RecvBufferSize);
Assert.True(ns.Running);
}
finally
@@ -87,7 +54,7 @@ public class NetStateSendBufferTests
[SkippableFact]
public void Account_OnTheLoginServer_KeepsTheInitialBuffers()
{
- Skip.If(NetState.InitialSendBufferSize == 0);
+ NetTestHelpers.RequirePreAuthBuffers();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -108,11 +75,11 @@ public class NetStateSendBufferTests
public void Send_BeyondTheInitialBuffer_BeforeCredentials_IsExhausted()
{
// Nothing promotes before credentials verify: the 4 KiB ring is the whole pre-auth budget
- Skip.If(NetState.InitialSendBufferSize == 0);
+ NetTestHelpers.RequirePreAuthBuffers();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
- var data = Pattern(NetState.InitialSendBufferSize + 512, 7);
+ var data = NetTestHelpers.Pattern(NetState.InitialSendBufferSize + 512, 7);
ns.Send(data);
Assert.Contains("exhausted", ns._disconnectReason, StringComparison.OrdinalIgnoreCase);
@@ -123,7 +90,7 @@ public class NetStateSendBufferTests
// Once it completes the NetState is disposed and _socket goes null, so nothing below
// this point may touch it.
client.Close();
- SliceUntil(() => !ns.Running);
+ NetTestHelpers.SliceUntil(() => !ns.Running);
}
finally
{
@@ -137,19 +104,19 @@ public class NetStateSendBufferTests
{
// The late-verdict path from Account_AssignedAfterTheStateFlipped_StillPromotes: an account
// attached while the setter's own gate does not fire must still let the send path promote
- Skip.If(NetState.InitialSendBufferSize == 0);
+ NetTestHelpers.RequirePreAuthBuffers();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
ns.Account = new MockAccount(); // _protocolState is still AwaitingSeed: the setter does not promote
- var data = Pattern(NetState.InitialSendBufferSize + 512, 7);
+ var data = NetTestHelpers.Pattern(NetState.InitialSendBufferSize + 512, 7);
ns.Send(data);
Assert.True(ns.Running);
Assert.Equal(string.Empty, ns._disconnectReason);
Assert.Equal(NetState.SendBufferSize, ns._socket.SendBuffer.PhysicalSize);
Assert.False(ns._sendBufferGrown); // promotion is not growth: nothing to shrink later
- Assert.Equal(data, ReadAll(client, data.Length));
+ Assert.Equal(data, NetTestHelpers.ReadAll(client, data.Length));
}
finally
{
@@ -161,12 +128,12 @@ public class NetStateSendBufferTests
[SkippableFact]
public void Send_BacklogBeforeCredentials_IsExhaustedInsteadOfPromoted()
{
- Skip.If(NetState.InitialSendBufferSize == 0);
+ NetTestHelpers.RequirePreAuthBuffers();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
// No Slice() between sends: nothing drains, so a second write finds a non-empty initial buffer
- var half = Pattern(NetState.InitialSendBufferSize / 2, 11);
+ var half = NetTestHelpers.Pattern(NetState.InitialSendBufferSize / 2, 11);
ns.Send(half);
ns.Send(half);
ns.Send(half); // cannot fit, buffer not empty, no account: refused
@@ -179,7 +146,7 @@ public class NetStateSendBufferTests
// Once it completes the NetState is disposed and _socket goes null, so nothing below
// this point may touch it.
client.Close();
- SliceUntil(() => !ns.Running);
+ NetTestHelpers.SliceUntil(() => !ns.Running);
}
finally
{
@@ -191,7 +158,7 @@ public class NetStateSendBufferTests
[SkippableFact]
public void Account_AssignedAfterTheStateFlipped_StillPromotes()
{
- Skip.If(NetState.InitialSendBufferSize == 0);
+ NetTestHelpers.RequirePreAuthBuffers();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -211,7 +178,7 @@ public class NetStateSendBufferTests
{
var randomLength = compressedLength * 5 / 8;
var data = new byte[compressedLength * 8];
- Pattern(randomLength, seed).CopyTo(data, 0);
+ NetTestHelpers.Pattern(randomLength, seed).CopyTo(data, 0);
var scratch = new byte[compressedLength * 2 + 16];
for (var length = randomLength; length < data.Length; length++)
@@ -228,47 +195,12 @@ public class NetStateSendBufferTests
throw new InvalidOperationException($"No chunk compresses to exactly {compressedLength} bytes");
}
- private static byte[] ReadAll(Socket client, int length)
- {
- var received = new byte[length];
- var total = 0;
- var deadline = Stopwatch.StartNew();
- while (total < length && deadline.ElapsedMilliseconds < 10000)
- {
- NetState.Slice();
-
- // Poll takes microseconds, not milliseconds
- if (client.Poll(50_000, SelectMode.SelectRead))
- {
- var read = client.Receive(received, total, length - total, SocketFlags.None);
- Assert.NotEqual(0, read);
- total += read;
- }
- }
-
- Assert.Equal(length, total);
- return received;
- }
-
// Peer receipt does not free the buffer; the completion must land first
- private static void WaitForDrain(NetState ns)
- {
- var deadline = Stopwatch.StartNew();
- while (deadline.ElapsedMilliseconds < 10000)
- {
- NetState.Slice();
-
- if (ns._socket.SendBuffer.ReadableBytes == 0 && ns._socket.SendBuffer.InFlightBytes == 0)
- {
- break;
- }
-
- Thread.Sleep(5);
- }
-
- Assert.Equal(0, ns._socket.SendBuffer.ReadableBytes);
- Assert.Equal(0, ns._socket.SendBuffer.InFlightBytes);
- }
+ private static void WaitForDrain(NetState ns) =>
+ NetTestHelpers.SliceUntil(
+ () => ns._socket.SendBuffer.ReadableBytes == 0 && ns._socket.SendBuffer.InFlightBytes == 0,
+ 10000
+ );
[Fact]
public void Send_GrowsInsteadOfDisconnecting_AndStreamStaysIntact()
@@ -283,7 +215,7 @@ public class NetStateSendBufferTests
var expected = new byte[chunk * 6];
for (var i = 0; i < 6; i++)
{
- var data = Pattern(chunk, i);
+ var data = NetTestHelpers.Pattern(chunk, i);
data.CopyTo(expected, i * chunk);
ns.Send(data);
}
@@ -293,7 +225,7 @@ public class NetStateSendBufferTests
Assert.True(ns._socket.SendBuffer.PhysicalSize > baseSize);
Assert.True(ns._sendBufferGrown);
- Assert.Equal(expected, ReadAll(client, expected.Length));
+ Assert.Equal(expected, NetTestHelpers.ReadAll(client, expected.Length));
}
finally
{
@@ -318,7 +250,7 @@ public class NetStateSendBufferTests
var expectedLength = 0;
for (var i = 0; i < 6; i++)
{
- var data = Pattern(chunkLength, i + 100);
+ var data = NetTestHelpers.Pattern(chunkLength, i + 100);
var compressedLength = NetworkCompression.Compress(data, scratch);
Assert.True(compressedLength > 0);
scratch.AsSpan(0, compressedLength).CopyTo(expected.AsSpan(expectedLength));
@@ -332,7 +264,7 @@ public class NetStateSendBufferTests
Assert.True(ns._sendBufferGrown);
Array.Resize(ref expected, expectedLength);
- Assert.Equal(expected, ReadAll(client, expected.Length));
+ Assert.Equal(expected, NetTestHelpers.ReadAll(client, expected.Length));
}
finally
{
@@ -365,7 +297,7 @@ public class NetStateSendBufferTests
while (ns._socket.SendBuffer.WritableBytes > 4096)
{
var writable = ns._socket.SendBuffer.WritableBytes;
- SendAndRecord(Pattern(Math.Min(baseSize / 8, (writable - 2048) * 8 / 11), seed++));
+ SendAndRecord(NetTestHelpers.Pattern(Math.Min(baseSize / 8, (writable - 2048) * 8 / 11), seed++));
}
// Full; no span to compress into
@@ -381,7 +313,7 @@ public class NetStateSendBufferTests
Assert.Equal(baseSize * 2, ns._socket.SendBuffer.PhysicalSize);
Assert.True(ns._sendBufferGrown);
- Assert.Equal(expected.ToArray(), ReadAll(client, expected.Count));
+ Assert.Equal(expected.ToArray(), NetTestHelpers.ReadAll(client, expected.Count));
}
finally
{
@@ -401,8 +333,8 @@ public class NetStateSendBufferTests
// A small peer buffer keeps the first send in flight through the growth
client.ReceiveBufferSize = 4096;
- var first = Pattern(baseSize / 2, 300);
- var second = Pattern(baseSize / 2 + 1, 301);
+ var first = NetTestHelpers.Pattern(baseSize / 2, 300);
+ var second = NetTestHelpers.Pattern(baseSize / 2 + 1, 301);
ns.Send(first);
NetState.Slice(); // posts the first send
@@ -419,7 +351,7 @@ public class NetStateSendBufferTests
first.CopyTo(expected, 0);
second.CopyTo(expected, first.Length);
- Assert.Equal(expected, ReadAll(client, expected.Length));
+ Assert.Equal(expected, NetTestHelpers.ReadAll(client, expected.Length));
}
finally
{
@@ -439,11 +371,11 @@ public class NetStateSendBufferTests
var chunk = baseSize / 4;
for (var i = 0; i < 6; i++)
{
- ns.Send(Pattern(chunk, i));
+ ns.Send(NetTestHelpers.Pattern(chunk, i));
}
Assert.True(ns._sendBufferGrown);
- ReadAll(client, chunk * 6);
+ NetTestHelpers.ReadAll(client, chunk * 6);
WaitForDrain(ns);
var grewAt = ns._sendBufferGrewAt;
@@ -471,11 +403,11 @@ public class NetStateSendBufferTests
var chunk = baseSize / 4;
for (var i = 0; i < 6; i++)
{
- ns.Send(Pattern(chunk, i + 400));
+ ns.Send(NetTestHelpers.Pattern(chunk, i + 400));
}
Assert.True(ns._sendBufferGrown);
- ReadAll(client, chunk * 6);
+ NetTestHelpers.ReadAll(client, chunk * 6);
WaitForDrain(ns);
// The sweep shrinks; nothing here calls TryShrinkSendBuffer
@@ -504,7 +436,7 @@ public class NetStateSendBufferTests
var writes = NetState.MaxSendBufferSize / chunk + 1;
for (var i = 0; i < writes; i++)
{
- ns.Send(Pattern(chunk, i));
+ ns.Send(NetTestHelpers.Pattern(chunk, i));
}
Assert.Contains("Send buffer exhausted", ns._disconnectReason);
@@ -532,7 +464,7 @@ public class NetStateSendBufferTests
for (var i = 0; i < 6; i++)
{
- ns.Send(Pattern(baseSize / 4, i));
+ ns.Send(NetTestHelpers.Pattern(baseSize / 4, i));
}
Assert.Equal(baseSize, ns._socket.SendBuffer.PhysicalSize);
@@ -558,7 +490,7 @@ public class NetStateSendBufferTests
NetState.Slice(); // handoff
// CannotSendPackets short-circuits before any growth
- ns.Send(Pattern(baseSize * 2, 500));
+ ns.Send(NetTestHelpers.Pattern(baseSize * 2, 500));
Assert.Equal(baseSize, ns._socket.SendBuffer.PhysicalSize);
Assert.False(ns._sendBufferGrown);
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs
index 6d8bd9fd9..c6a8b43a4 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs
@@ -1,7 +1,5 @@
using System;
-using System.Collections.Generic;
using System.Net;
-using Server.Accounting;
using Server.Network;
using Xunit;
@@ -10,60 +8,6 @@ namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
public class AccountPacketTests
{
- private class MockedAccount : 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; }
- public int Count { get; }
-
- private Dictionary _mobiles = new();
-
- public Mobile this[int index]
- {
- get => _mobiles[index];
- set
- {
- _mobiles[index] = value;
- if (value != null)
- {
- value.Account = this;
- }
- }
- }
-
- public DateTime Created { get; set; }
- public Serial Serial { get; }
- public void Deserialize(IGenericReader reader) => throw new NotImplementedException();
-
- 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();
- }
-
[Fact]
public void TestChangeCharacter()
{
@@ -75,58 +19,38 @@ public class AccountPacketTests
secondMobile.DefaultMobileInit();
secondMobile.RawName = null;
- var account = new MockedAccount
+ var account = new MockAccount
{
[0] = firstMobile,
[1] = null,
[2] = secondMobile
};
- // var account = new MockAccount(new[] { firstMobile, null, secondMobile });
- var expected = new ChangeCharacter(account).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendChangeCharacter(account);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ChangeCharacter(account), ns => ns.SendChangeCharacter(account));
}
[Fact]
public void TestClientVersionReq()
{
- var expected = new ClientVersionReq().Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
-
- ns.SendClientVersionRequest();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ClientVersionReq(), ns => ns.SendClientVersionRequest());
}
[Fact]
public void TestDeleteResult()
{
- var expected = new DeleteResult(DeleteResultType.BadRequest).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCharacterDeleteResult(DeleteResultType.BadRequest);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DeleteResult(DeleteResultType.BadRequest),
+ ns => ns.SendCharacterDeleteResult(DeleteResultType.BadRequest)
+ );
}
[Fact]
public void TestPopupMessage()
{
- var expected = new PopupMessage(PMMessage.LoginSyncError).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPopupMessage(PMMessage.LoginSyncError);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new PopupMessage(PMMessage.LoginSyncError),
+ ns => ns.SendPopupMessage(PMMessage.LoginSyncError)
+ );
}
[Theory, InlineData(ProtocolChanges.Version70610), InlineData(ProtocolChanges.Version6000)]
@@ -136,7 +60,7 @@ public class AccountPacketTests
firstMobile.DefaultMobileInit();
firstMobile.Name = "Test Mobile";
- var account = new MockedAccount
+ var account = new MockAccount
{
[0] = firstMobile,
[1] = null,
@@ -168,25 +92,13 @@ public class AccountPacketTests
m.Direction = Direction.Down;
m.LogoutMap = Map.Felucca;
- var expected = new LoginConfirm(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendLoginConfirmation(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new LoginConfirm(m), ns => ns.SendLoginConfirmation(m));
}
[Fact]
public void TestLoginComplete()
{
- var expected = new LoginComplete().Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendLoginComplete();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new LoginComplete(), ns => ns.SendLoginComplete());
}
[Fact]
@@ -196,7 +108,7 @@ public class AccountPacketTests
firstMobile.DefaultMobileInit();
firstMobile.RawName = "Test Mobile";
- var account = new MockedAccount
+ var account = new MockAccount
{
[0] = null,
[1] = firstMobile,
@@ -205,13 +117,7 @@ public class AccountPacketTests
[4] = null
};
- var expected = new CharacterListUpdate(account).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCharacterListUpdate(account);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new CharacterListUpdate(account), ns => ns.SendCharacterListUpdate(account));
}
[Fact]
@@ -221,7 +127,7 @@ public class AccountPacketTests
firstMobile.DefaultMobileInit();
firstMobile.Name = "Test Mobile";
- var account = new MockedAccount
+ var account = new MockAccount
{
[0] = null,
[1] = firstMobile,
@@ -235,17 +141,16 @@ public class AccountPacketTests
new CityInfo("Test City", "Test Building", 50, 100, 10, -10)
};
- var expected = new CharacterList(account, info).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.CityInfo = info;
- ns.Account = account;
- ns.ProtocolChanges = ProtocolChanges.Version70130;
-
- ns.SendCharacterList();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new CharacterList(account, info),
+ ns =>
+ {
+ ns.CityInfo = info;
+ ns.Account = account;
+ ns.SendCharacterList();
+ },
+ ProtocolChanges.Version70130
+ );
}
[Fact]
@@ -255,7 +160,7 @@ public class AccountPacketTests
firstMobile.DefaultMobileInit();
firstMobile.Name = "Test Mobile";
- var account = new MockedAccount
+ var account = new MockAccount
{
[0] = null,
[1] = firstMobile,
@@ -269,29 +174,23 @@ public class AccountPacketTests
new CityInfo("Test City", "Test Building", 50, 100, 10, -10)
};
- var expected = new CharacterListOld(account, info).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.CityInfo = info;
- ns.Account = account;
-
- ns.SendCharacterList();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new CharacterListOld(account, info),
+ ns =>
+ {
+ ns.CityInfo = info;
+ ns.Account = account;
+ ns.SendCharacterList();
+ }
+ );
}
[Fact]
public void TestAccountLoginRej()
{
var reason = ALRReason.BadComm;
- var expected = new AccountLoginRej(reason).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendAccountLoginRejected(reason);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new AccountLoginRej(reason), ns => ns.SendAccountLoginRejected(reason));
}
[Fact]
@@ -302,15 +201,14 @@ public class AccountPacketTests
new ServerInfo("Test Server", 0, TimeZoneInfo.Local, IPEndPoint.Parse("127.0.0.1"))
};
- var expected = new AccountLoginAck(info).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ServerInfo = info;
-
- ns.SendAccountLoginAck();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new AccountLoginAck(info),
+ ns =>
+ {
+ ns.ServerInfo = info;
+ ns.SendAccountLoginAck();
+ }
+ );
}
[Fact]
@@ -319,13 +217,6 @@ public class AccountPacketTests
var si = new ServerInfo("Test Server", 0, TimeZoneInfo.Local, IPEndPoint.Parse("127.0.0.1"));
const int authId = 0x123456;
- var expected = new PlayServerAck(si, authId).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
-
- ns.SendPlayServerAck(si, authId);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new PlayServerAck(si, authId), ns => ns.SendPlayServerAck(si, authId));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/CombatPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/CombatPacketTests.cs
index a885cae9e..de2401a0c 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/CombatPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/CombatPacketTests.cs
@@ -12,25 +12,13 @@ public class CombatPacketTests
var attacker = (Serial)0x1024;
var defender = (Serial)0x2048;
- var expected = new Swing(attacker, defender).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSwing(attacker, defender);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new Swing(attacker, defender), ns => ns.SendSwing(attacker, defender));
}
[Theory, InlineData(true), InlineData(false)]
public void TestSetWarMode(bool warmode)
{
- var expected = new SetWarMode(warmode).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSetWarMode(warmode);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new SetWarMode(warmode), ns => ns.SendSetWarMode(warmode));
}
[Fact]
@@ -38,12 +26,6 @@ public class CombatPacketTests
{
var serial = (Serial)0x1024;
- var expected = new ChangeCombatant(serial).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendChangeCombatant(serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ChangeCombatant(serial), ns => ns.SendChangeCombatant(serial));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs
index 00dfc4054..98229c6fe 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs
@@ -14,13 +14,7 @@ public class ContainerPacketTests
var serial = (Serial)0x1024;
ushort gumpId = 100;
- var expected = new ContainerDisplay(serial, gumpId).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayContainer(serial, gumpId);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ContainerDisplay(serial, gumpId), ns => ns.SendDisplayContainer(serial, gumpId));
}
[Fact]
@@ -29,14 +23,11 @@ public class ContainerPacketTests
var serial = (Serial)0x1024;
ushort gumpId = 100;
- var expected = new ContainerDisplayHS(serial, gumpId).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ns.ProtocolChanges | ProtocolChanges.ContainerGridLines | ProtocolChanges.HighSeas;
- ns.SendDisplayContainer(serial, gumpId);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new ContainerDisplayHS(serial, gumpId),
+ ns => ns.SendDisplayContainer(serial, gumpId),
+ ProtocolChanges.ContainerGridLines | ProtocolChanges.HighSeas
+ );
}
[Fact]
@@ -44,13 +35,7 @@ public class ContainerPacketTests
{
var serial = (Serial)0x1024;
- var expected = new DisplaySpellbook(serial).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplaySpellbook(serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DisplaySpellbook(serial), ns => ns.SendDisplaySpellbook(serial));
}
[Fact]
@@ -58,14 +43,11 @@ public class ContainerPacketTests
{
var serial = (Serial)0x1024;
- var expected = new DisplaySpellbookHS(serial).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ns.ProtocolChanges | ProtocolChanges.ContainerGridLines | ProtocolChanges.HighSeas;
- ns.SendDisplaySpellbook(serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplaySpellbookHS(serial),
+ ns => ns.SendDisplaySpellbook(serial),
+ ProtocolChanges.ContainerGridLines | ProtocolChanges.HighSeas
+ );
}
[Fact]
@@ -76,17 +58,20 @@ public class ContainerPacketTests
ushort offset = 10;
ulong content = 0x123456789ABCDEF0;
var opl = ObjectPropertyList.Enabled;
-
- var expected = new NewSpellbookContent(serial, graphic, offset, content).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ns.ProtocolChanges | ProtocolChanges.ContainerGridLines | ProtocolChanges.NewSpellbook;
ObjectPropertyList.Enabled = true;
- ns.SendSpellbookContent(serial, graphic, offset, content);
- ObjectPropertyList.Enabled = opl;
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ try
+ {
+ PacketAssert.Sends(
+ new NewSpellbookContent(serial, graphic, offset, content),
+ ns => ns.SendSpellbookContent(serial, graphic, offset, content),
+ ProtocolChanges.ContainerGridLines | ProtocolChanges.NewSpellbook
+ );
+ }
+ finally
+ {
+ ObjectPropertyList.Enabled = opl;
+ }
}
[Fact]
@@ -97,13 +82,10 @@ public class ContainerPacketTests
ushort graphic = 100;
ulong content = 0x123456789ABCDEF0;
- var expected = new SpellbookContent(serial, offset, content).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSpellbookContent(serial, graphic, offset, content);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new SpellbookContent(serial, offset, content),
+ ns => ns.SendSpellbookContent(serial, graphic, offset, content)
+ );
}
[Fact]
@@ -114,14 +96,11 @@ public class ContainerPacketTests
ushort graphic = 100;
ulong content = 0x123456789ABCDEF0;
- var expected = new SpellbookContent6017(serial, offset, content).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
- ns.SendSpellbookContent(serial, graphic, offset, content);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new SpellbookContent6017(serial, offset, content),
+ ns => ns.SendSpellbookContent(serial, graphic, offset, content),
+ ProtocolChanges.ContainerGridLines
+ );
}
[Fact]
@@ -130,13 +109,7 @@ public class ContainerPacketTests
var serial = (Serial)0x1024;
var item = new Item(serial);
- var expected = new ContainerContentUpdate(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendContainerContentUpdate(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ContainerContentUpdate(item), ns => ns.SendContainerContentUpdate(item));
}
[Fact]
@@ -145,66 +118,17 @@ public class ContainerPacketTests
var serial = (Serial)0x1024;
var item = new Item(serial);
- var expected = new ContainerContentUpdate6017(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
- ns.SendContainerContentUpdate(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new ContainerContentUpdate6017(item),
+ ns => ns.SendContainerContentUpdate(item),
+ ProtocolChanges.ContainerGridLines
+ );
}
- [Fact]
- public void TestContainerContent()
+ // An administrator beholder on the container's map, so every item is visible and sent.
+ private static (Container Container, Mobile Beholder) CreateViewedContainer(int itemCount)
{
var cont = new Container(World.NewItem);
- cont.AddItem(new Item(World.NewItem));
- cont.Map = Map.Felucca;
-
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.AccessLevel = AccessLevel.Administrator;
- m.Map = Map.Felucca;
-
- var expected = new ContainerContent(m, cont).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendContainerContent(m, cont);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
- }
-
- [Fact]
- public void TestContainerContent6017()
- {
- var cont = new Container(World.NewItem);
- cont.AddItem(new Item(World.NewItem));
- cont.Map = Map.Felucca;
-
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.AccessLevel = AccessLevel.Administrator;
- m.Map = Map.Felucca;
-
- var expected = new ContainerContent6017(m, cont).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
- ns.SendContainerContent(m, cont);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
- }
-
- [Fact]
- public void TestContainerContent_CapsAtProtocolLimit()
- {
- var cont = new Container(World.NewItem);
-
- // Past the 3448-entry cap (19-byte entries) so truncation engages.
- const int itemCount = 5000;
for (var i = 0; i < itemCount; i++)
{
cont.AddItem(new Item(World.NewItem));
@@ -217,9 +141,62 @@ public class ContainerPacketTests
m.AccessLevel = AccessLevel.Administrator;
m.Map = Map.Felucca;
+ return (cont, m);
+ }
+
+ [Fact]
+ public void TestContainerContent()
+ {
+ var (cont, m) = CreateViewedContainer(1);
+
+ try
+ {
+ PacketAssert.Sends(new ContainerContent(m, cont), ns => ns.SendContainerContent(m, cont));
+ }
+ finally
+ {
+ cont.Delete();
+ m.Delete();
+ }
+ }
+
+ [Fact]
+ public void TestContainerContent6017()
+ {
+ var (cont, m) = CreateViewedContainer(1);
+
+ try
+ {
+ PacketAssert.Sends(
+ new ContainerContent6017(m, cont),
+ ns => ns.SendContainerContent(m, cont),
+ ProtocolChanges.ContainerGridLines
+ );
+ }
+ finally
+ {
+ cont.Delete();
+ m.Delete();
+ }
+ }
+
+ // 0x3C's length and count fields are 16-bit: 19-byte entries cap at 3448, 20-byte grid-line entries at 3276.
+ [Theory]
+ [InlineData(false, 3448, 65517)]
+ [InlineData(true, 3276, 65525)]
+ public void TestContainerContent_CapsAtProtocolLimit(bool gridLines, int maxEntries, int maxLength)
+ {
+ // One item past the cap is enough to make truncation engage.
+ var (cont, m) = CreateViewedContainer(maxEntries + 1);
+
try
{
using var ns = PacketTestUtilities.CreateTestNetState();
+ if (gridLines)
+ {
+ ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
+ }
+
// A ~64KB packet exceeds the 4KB pre-auth send buffer; an account lets Send() promote it
// on demand instead of exhausting the buffer and disconnecting.
ns.Account = new MockAccount();
@@ -233,57 +210,9 @@ public class ContainerPacketTests
var length = BinaryPrimitives.ReadUInt16BigEndian(span[1..3]);
var count = BinaryPrimitives.ReadUInt16BigEndian(span[3..5]);
- const int entrySize = 19; // no grid lines negotiated on this NetState
- var expectedMaxEntries = (ushort.MaxValue - 5) / entrySize;
-
- Assert.Equal(65517, length);
+ Assert.Equal(maxLength, length);
Assert.Equal(length, span.Length);
- Assert.Equal(expectedMaxEntries, count);
- }
- finally
- {
- cont.Delete();
- m.Delete();
- }
- }
-
- [Fact]
- public void TestContainerContent_CapsAtProtocolLimit_WithGridLines()
- {
- var cont = new Container(World.NewItem);
-
- // Past the 3276-entry cap (20-byte entries with grid lines) so truncation engages.
- const int itemCount = 5000;
- for (var i = 0; i < itemCount; i++)
- {
- cont.AddItem(new Item(World.NewItem));
- }
-
- cont.Map = Map.Felucca;
-
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.AccessLevel = AccessLevel.Administrator;
- m.Map = Map.Felucca;
-
- try
- {
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
- ns.Account = new MockAccount();
-
- var ex = Record.Exception(() => ns.SendContainerContent(m, cont));
- Assert.Null(ex);
-
- var span = ns.SendBuffer.GetReadSpan();
- Assert.Equal((byte)0x3C, span[0]);
-
- var length = BinaryPrimitives.ReadUInt16BigEndian(span[1..3]);
- var count = BinaryPrimitives.ReadUInt16BigEndian(span[3..5]);
-
- Assert.Equal(65525, length);
- Assert.Equal(length, span.Length);
- Assert.Equal(3276, count);
+ Assert.Equal(maxEntries, count);
}
finally
{
@@ -295,32 +224,13 @@ public class ContainerPacketTests
[Fact]
public void TestContainerContent_RentedBuffer_MatchesReferencePacket()
{
- var cont = new Container(World.NewItem);
-
// Below the entry cap but past the 1024-byte stackalloc threshold, so SendContainerContent
// rents its buffer instead of using the stack.
- const int itemCount = 100;
- for (var i = 0; i < itemCount; i++)
- {
- cont.AddItem(new Item(World.NewItem));
- }
-
- cont.Map = Map.Felucca;
-
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.AccessLevel = AccessLevel.Administrator;
- m.Map = Map.Felucca;
+ var (cont, m) = CreateViewedContainer(100);
try
{
- var expected = new ContainerContent(m, cont).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendContainerContent(m, cont);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ContainerContent(m, cont), ns => ns.SendContainerContent(m, cont));
}
finally
{
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs
index 5f31a49d4..bda7ad009 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs
@@ -11,13 +11,7 @@ public class DamagePacketTests
{
var serial = (Serial)0x1024;
- var expected = new DamagePacketOld(serial, inputAmount).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDamage(serial, inputAmount);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DamagePacketOld(serial, inputAmount), ns => ns.SendDamage(serial, inputAmount));
}
[Theory, InlineData(10), InlineData(-5), InlineData(1024), InlineData(100000)]
@@ -25,14 +19,10 @@ public class DamagePacketTests
{
var serial = (Serial)0x1024;
- var expected = new DamagePacket(serial, inputAmount).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ProtocolChanges.DamagePacket;
-
- ns.SendDamage(serial, inputAmount);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DamagePacket(serial, inputAmount),
+ ns => ns.SendDamage(serial, inputAmount),
+ ProtocolChanges.DamagePacket
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPacketTests.cs
index 7ade294f6..e9395948d 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPacketTests.cs
@@ -7,18 +7,13 @@ namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
public class EffectPackets
{
- [Theory, InlineData(10, 1000, 10, 5)]
- public void TestSoundEffect(ushort soundID, int x, int y, int z)
+ [Fact]
+ public void TestSoundEffect()
{
- var p = new Point3D(x, y, z);
+ const ushort soundID = 10;
+ var p = new Point3D(1000, 10, 5);
- var expected = new PlaySound(soundID, p).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSoundEffect(soundID, p);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new PlaySound(soundID, p), ns => ns.SendSoundEffect(soundID, p));
}
[Fact]
@@ -94,13 +89,7 @@ public class EffectPackets
[Theory, InlineData(ScreenEffectType.DarkFlash), InlineData(ScreenEffectType.FadeInOut)]
public void TestScreenEffect(ScreenEffectType screenType)
{
- var expected = new ScreenEffect(screenType).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendScreenEffect(screenType);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ScreenEffect(screenType), ns => ns.SendScreenEffect(screenType));
}
[Fact]
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPackets.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPackets.cs
index e18256383..2899569f8 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPackets.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPackets.cs
@@ -76,157 +76,6 @@ public class HuedEffect : Packet
}
}
-public sealed class TargetParticleEffect : ParticleEffect
-{
- public TargetParticleEffect(
- IEntity e, int itemID, int speed, int duration, int hue, int renderMode, int effect,
- int layer, int unknown
- ) : base(
- EffectType.FixedFrom,
- e.Serial,
- Serial.Zero,
- itemID,
- e.Location,
- e.Location,
- speed,
- duration,
- true,
- false,
- hue,
- renderMode,
- effect,
- 1,
- 0,
- e.Serial,
- layer,
- unknown
- )
- {
- }
-}
-
-public sealed class TargetEffect : HuedEffect
-{
- public TargetEffect(IEntity e, int itemID, int speed, int duration, int hue, int renderMode) : base(
- EffectType.FixedFrom,
- e.Serial,
- Serial.Zero,
- itemID,
- e.Location,
- e.Location,
- speed,
- duration,
- true,
- false,
- hue,
- renderMode
- )
- {
- }
-}
-
-public sealed class LocationParticleEffect : ParticleEffect
-{
- public LocationParticleEffect(
- IEntity e, int itemID, int speed, int duration, int hue, int renderMode, int effect,
- int unknown
- ) : base(
- EffectType.FixedXYZ,
- e.Serial,
- Serial.Zero,
- itemID,
- e.Location,
- e.Location,
- speed,
- duration,
- true,
- false,
- hue,
- renderMode,
- effect,
- 1,
- 0,
- e.Serial,
- 255,
- unknown
- )
- {
- }
-}
-
-public sealed class LocationEffect : HuedEffect
-{
- public LocationEffect(IPoint3D p, int itemID, int speed, int duration, int hue, int renderMode) : base(
- EffectType.FixedXYZ,
- Serial.Zero,
- Serial.Zero,
- itemID,
- p,
- p,
- speed,
- duration,
- true,
- false,
- hue,
- renderMode
- )
- {
- }
-}
-
-public sealed class MovingParticleEffect : ParticleEffect
-{
- public MovingParticleEffect(
- IEntity from, IEntity to, int itemID, int speed, int duration, bool fixedDirection,
- bool explodes, int hue, int renderMode, int effect, int explodeEffect, int explodeSound, EffectLayer layer,
- int unknown
- ) : base(
- EffectType.Moving,
- from.Serial,
- to.Serial,
- itemID,
- from.Location,
- to.Location,
- speed,
- duration,
- fixedDirection,
- explodes,
- hue,
- renderMode,
- effect,
- explodeEffect,
- explodeSound,
- Serial.Zero,
- (int)layer,
- unknown
- )
- {
- }
-}
-
-public sealed class MovingEffect : HuedEffect
-{
- public MovingEffect(
- IEntity from, IEntity to, int itemID, int speed, int duration, bool fixedDirection,
- bool explodes, int hue, int renderMode
- ) : base(
- EffectType.Moving,
- from.Serial,
- to.Serial,
- itemID,
- from.Location,
- to.Location,
- speed,
- duration,
- fixedDirection,
- explodes,
- hue,
- renderMode
- )
- {
- }
-}
-
public class ScreenEffect : Packet
{
public ScreenEffect(ScreenEffectType type)
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs
index 8dc535e03..21933bc91 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs
@@ -31,19 +31,16 @@ public class EquipmentPacketTests
}
);
- var expected = new DisplayEquipmentInfo(item, info).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayEquipmentInfo(
- item.Serial,
- info.Number,
- info.Crafter?.RawName,
- info.Unidentified,
- new List(info.Attributes)
+ PacketAssert.Sends(
+ new DisplayEquipmentInfo(item, info),
+ ns => ns.SendDisplayEquipmentInfo(
+ item.Serial,
+ info.Number,
+ info.Crafter?.RawName,
+ info.Unidentified,
+ new List(info.Attributes)
+ )
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -54,12 +51,6 @@ public class EquipmentPacketTests
var item = new Item(World.NewItem) { Parent = m };
- var expected = new EquipUpdate(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendEquipUpdate(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new EquipUpdate(item), ns => ns.SendEquipUpdate(item));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs
index edbe6b668..febac5288 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs
@@ -6,17 +6,13 @@ namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
public class GumpPacketTests
{
- [Theory]
- [InlineData(100, 10)]
- public void TestCloseGump(int typeId, int buttonId)
+ [Fact]
+ public void TestCloseGump()
{
- var expected = new CloseGump(typeId, buttonId).Compile();
+ const int typeId = 100;
+ const int buttonId = 10;
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCloseGump(typeId, buttonId);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new CloseGump(typeId, buttonId), ns => ns.SendCloseGump(typeId, buttonId));
}
[Fact]
@@ -27,13 +23,10 @@ public class GumpPacketTests
const string unknownString = "This is an unknown string";
const string caption = "This is a caption";
- var expected = new DisplaySignGump(gumpSerial, gumpId, unknownString, caption).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplaySignGump(gumpSerial, gumpId, unknownString, caption);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplaySignGump(gumpSerial, gumpId, unknownString, caption),
+ ns => ns.SendDisplaySignGump(gumpSerial, gumpId, unknownString, caption)
+ );
}
[Fact]
@@ -41,13 +34,7 @@ public class GumpPacketTests
{
var gump = new NameChangeDeedGump();
- using var ns = PacketTestUtilities.CreateTestNetState();
-
- var expected = gump.Compile(ns).Compile();
- ns.SendGump(gump);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(ns => gump.Compile(ns), ns => ns.SendGump(gump));
}
[Fact]
@@ -60,13 +47,7 @@ public class GumpPacketTests
var gump = new AdminGump(m, AdminGumpPage.Clients);
- using var ns = PacketTestUtilities.CreateTestNetState();
-
- var expected = gump.Compile(ns).Compile();
- ns.SendGump(gump);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(ns => gump.Compile(ns), ns => ns.SendGump(gump));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs
index 27bad8aaf..f9d5cb948 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs
@@ -1,116 +1,75 @@
+using System;
using Server.Network;
using Xunit;
namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
-public class ItemPacketTests
+public class ItemPacketTests : IDisposable
{
+ private const int TestItemId = 1;
+
+ // TileData is global and holds real client data when client files are loaded, so each test restores it.
+ private readonly ItemData _originalItemData;
+
+ public ItemPacketTests()
+ {
+ _originalItemData = TileData.ItemTable[TestItemId];
+ TileData.ItemTable[TestItemId] = new ItemData("Test Item Data", TileFlag.Generic, 1, 1, 1, 1, 1, 1);
+ }
+
+ public void Dispose() => TileData.ItemTable[TestItemId] = _originalItemData;
+
[Fact]
public void TestWorldItemPacket()
{
var serial = (Serial)0x1024;
- var itemId = 1;
-
- // Move to fixture
- TileData.ItemTable[itemId] = new ItemData(
- "Test Item Data",
- TileFlag.Generic,
- 1,
- 1,
- 1,
- 1,
- 1,
- 1
- );
var item = new Item(serial)
{
- ItemID = itemId,
+ ItemID = TestItemId,
Hue = 0x1024,
Amount = 10,
Location = new Point3D(1000, 100, -10),
Direction = Direction.Left
};
- var expected = new WorldItem(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendWorldItem(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new WorldItem(item), ns => ns.SendWorldItem(item));
}
[Fact]
public void TestWorldItemSAPacket()
{
var serial = (Serial)0x1024;
- ushort itemId = 1;
-
- // Move to fixture
- TileData.ItemTable[itemId] = new ItemData(
- "Test Item Data",
- TileFlag.Generic,
- 1,
- 1,
- 1,
- 1,
- 1,
- 1
- );
var item = new Item(serial)
{
- ItemID = itemId,
+ ItemID = TestItemId,
Hue = 0x1024,
Amount = 10,
Location = new Point3D(1000, 100, -10)
};
- var expected = new WorldItemSA(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ProtocolChanges.StygianAbyss;
- ns.SendWorldItem(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new WorldItemSA(item), ns => ns.SendWorldItem(item), ProtocolChanges.StygianAbyss);
}
[Fact]
public void TestWorldItemHSPacket()
{
var serial = (Serial)0x1024;
- var itemId = 1;
-
- // Move to fixture
- TileData.ItemTable[itemId] = new ItemData(
- "Test Item Data",
- TileFlag.Generic,
- 1,
- 1,
- 1,
- 1,
- 1,
- 1
- );
var item = new Item(serial)
{
- ItemID = itemId,
+ ItemID = TestItemId,
Hue = 0x1024,
Amount = 10,
Location = new Point3D(1000, 100, -10)
};
- var expected = new WorldItemHS(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ProtocolChanges.StygianAbyss | ProtocolChanges.HighSeas;
- ns.SendWorldItem(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new WorldItemHS(item),
+ ns => ns.SendWorldItem(item),
+ ProtocolChanges.StygianAbyss | ProtocolChanges.HighSeas
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPackets.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPackets.cs
index 084ba4587..90bd89dfb 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPackets.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPackets.cs
@@ -187,21 +187,3 @@ public sealed class WorldItemHS : Packet
Stream.Write((short)0x00); // ??
}
}
-
-public sealed class OPLInfo : Packet
-{
- /*public OPLInfo( ObjectPropertyList list ) : base( 0xBF )
- {
- EnsureCapacity( 13 );
-
- m_Stream.Write( (short) 0x10 );
- m_Stream.Write( (int) list.Entity.Serial );
- m_Stream.Write( (int) list.Hash );
- }*/
-
- public OPLInfo(Serial serial, int hash) : base(0xDC, 9)
- {
- Stream.Write(serial);
- Stream.Write(hash);
- }
-}
\ No newline at end of file
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs
index c66a61ca7..22ed4c423 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs
@@ -10,13 +10,8 @@ public class LightPacketTests
public void TestGlobalLightLevel()
{
const byte lightLevel = 5;
- var expected = new GlobalLightLevel(lightLevel).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendGlobalLightLevel(lightLevel);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new GlobalLightLevel(lightLevel), ns => ns.SendGlobalLightLevel(lightLevel));
}
[Fact]
@@ -24,12 +19,10 @@ public class LightPacketTests
{
var serial = (Serial)0x1024;
byte lightLevel = 5;
- var expected = new PersonalLightLevel(serial, lightLevel).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPersonalLightLevel(serial, lightLevel);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new PersonalLightLevel(serial, lightLevel),
+ ns => ns.SendPersonalLightLevel(serial, lightLevel)
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs
index a438730e0..d563db252 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs
@@ -9,26 +9,13 @@ public class MapPatchesTests
[Fact]
public void TestMapPatches()
{
- using var ns = PacketTestUtilities.CreateTestNetState();
-
- var expected = new MapPatches().Compile();
-
- ns.SendMapPatches();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MapPatches(), ns => ns.SendMapPatches());
}
[Fact]
public void TestInvalidMapEnable()
{
- var expected = new InvalidMapEnable().Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendInvalidMap();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new InvalidMapEnable(), ns => ns.SendInvalidMap());
}
[Theory]
@@ -37,12 +24,7 @@ public class MapPatchesTests
public void TestMapChange(string mapName)
{
var map = Map.Parse(mapName);
- var expected = new MapChange(map).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMapChange(map);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MapChange(map), ns => ns.SendMapChange(map));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs
index 3da55c89e..c87355ac8 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs
@@ -42,13 +42,7 @@ public class MenuPacketTests
]
);
- var expected = new DisplayItemListMenu(menu).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayItemListMenu(menu);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DisplayItemListMenu(menu), ns => ns.SendDisplayItemListMenu(menu));
}
[Fact]
@@ -63,12 +57,7 @@ public class MenuPacketTests
]
);
- var expected = new DisplayQuestionMenu(menu).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayQuestionMenu(menu);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DisplayQuestionMenu(menu), ns => ns.SendDisplayQuestionMenu(menu));
}
[Theory]
@@ -84,18 +73,12 @@ public class MenuPacketTests
var item = new ContextMenuItem(World.NewItem, newPacket);
var menu = ContextMenuSystem.CreateContextMenu(m, item);
- var packet = newHaven && newPacket ? (Packet)new DisplayContextMenu(menu) : new DisplayContextMenuOld(menu);
- var expected = packet.Compile();
+ var expected = newHaven && newPacket ? (Packet)new DisplayContextMenu(menu) : new DisplayContextMenuOld(menu);
- using var ns = PacketTestUtilities.CreateTestNetState();
- if (newHaven)
- {
- ns.ProtocolChanges |= ProtocolChanges.NewHaven;
- }
-
- ns.SendDisplayContextMenu(menu);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ expected,
+ ns => ns.SendDisplayContextMenu(menu),
+ newHaven ? ProtocolChanges.NewHaven : ProtocolChanges.None
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MessagePacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MessagePacketTests.cs
index b5eb423f1..c1911db57 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MessagePacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MessagePacketTests.cs
@@ -19,31 +19,10 @@ public class MessageTests
var name = "Stuff";
var args = "Arguments";
- var expected = new MessageLocalized(
- serial,
- graphic,
- messageType,
- hue,
- font,
- number,
- name,
- args
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessageLocalized(
- serial,
- graphic,
- messageType,
- hue,
- font,
- number,
- name,
- args
+ PacketAssert.Sends(
+ new MessageLocalized(serial, graphic, messageType, hue, font, number, name, args),
+ ns => ns.SendMessageLocalized(serial, graphic, messageType, hue, font, number, name, args)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -58,15 +37,10 @@ public class MessageTests
var name = "Stuff";
var argValue = 42;
- var expected = new MessageLocalized(
- serial, graphic, messageType, hue, font, number, name, $"value: {argValue}"
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessageLocalized(serial, graphic, messageType, hue, font, number, name, $"value: {argValue}");
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new MessageLocalized(serial, graphic, messageType, hue, font, number, name, $"value: {argValue}"),
+ ns => ns.SendMessageLocalized(serial, graphic, messageType, hue, font, number, name, $"value: {argValue}")
+ );
}
[Fact]
@@ -83,35 +57,12 @@ public class MessageTests
var affixType = AffixType.System;
var affix = "Affix";
- var expected = new MessageLocalizedAffix(
- serial,
- graphic,
- messageType,
- hue,
- font,
- number,
- name,
- affixType,
- affix,
- args
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessageLocalizedAffix(
- serial,
- graphic,
- messageType,
- hue,
- font,
- number,
- name,
- affixType,
- affix,
- args
+ PacketAssert.Sends(
+ new MessageLocalizedAffix(serial, graphic, messageType, hue, font, number, name, affixType, affix, args),
+ ns => ns.SendMessageLocalizedAffix(
+ serial, graphic, messageType, hue, font, number, name, affixType, affix, args
+ )
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -128,18 +79,14 @@ public class MessageTests
var affix = "Affix";
var argValue = 42;
- var expected = new MessageLocalizedAffix(
- serial, graphic, messageType, hue, font, number, name, affixType, affix, $"value: {argValue}"
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessageLocalizedAffix(
- serial, graphic, messageType, hue, font, number, name, affixType, affix,
- $"value: {argValue}"
+ PacketAssert.Sends(
+ new MessageLocalizedAffix(
+ serial, graphic, messageType, hue, font, number, name, affixType, affix, $"value: {argValue}"
+ ),
+ ns => ns.SendMessageLocalizedAffix(
+ serial, graphic, messageType, hue, font, number, name, affixType, affix, $"value: {argValue}"
+ )
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -154,17 +101,10 @@ public class MessageTests
var name = "Stuff";
var who = "Alice";
- var expected = new UnicodeMessage(
- serial, graphic, messageType, hue, font, lang, name, $"hello, {who}"
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessage(
- serial, graphic, messageType, hue, font, false, lang, name, $"hello, {who}"
+ PacketAssert.Sends(
+ new UnicodeMessage(serial, graphic, messageType, hue, font, lang, name, $"hello, {who}"),
+ ns => ns.SendMessage(serial, graphic, messageType, hue, font, false, lang, name, $"hello, {who}")
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -178,31 +118,10 @@ public class MessageTests
var name = "Stuff";
var text = "Some Text";
- var expected = new AsciiMessage(
- serial,
- graphic,
- messageType,
- hue,
- font,
- name,
- text
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessage(
- serial,
- graphic,
- messageType,
- hue,
- font,
- true,
- null,
- name,
- text
+ PacketAssert.Sends(
+ new AsciiMessage(serial, graphic, messageType, hue, font, name, text),
+ ns => ns.SendMessage(serial, graphic, messageType, hue, font, true, null, name, text)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -217,32 +136,10 @@ public class MessageTests
var name = "Stuff";
var text = "Some Text";
- var expected = new UnicodeMessage(
- serial,
- graphic,
- messageType,
- hue,
- font,
- lang,
- name,
- text
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessage(
- serial,
- graphic,
- messageType,
- hue,
- font,
- false,
- lang,
- name,
- text
+ PacketAssert.Sends(
+ new UnicodeMessage(serial, graphic, messageType, hue, font, lang, name, text),
+ ns => ns.SendMessage(serial, graphic, messageType, hue, font, false, lang, name, text)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -251,13 +148,7 @@ public class MessageTests
var serial = (Serial)0x1024;
var serial2 = (Serial)0x2;
- var expected = new FollowMessage(serial, serial2).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendFollowMessage(serial, serial2);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new FollowMessage(serial, serial2), ns => ns.SendFollowMessage(serial, serial2));
}
[Fact]
@@ -266,33 +157,18 @@ public class MessageTests
var s = (Serial)0x100;
var text = "This is some testing text";
- var expected = new ObjectHelpResponse(s, text).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendHelpResponse(s, text);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ObjectHelpResponse(s, text), ns => ns.SendHelpResponse(s, text));
}
- internal class TestPrompt : Prompt
+ [Fact]
+ public void TestUnicodePrompt()
{
+ var prompt = new TestPrompt();
+
+ PacketAssert.Sends(new UnicodePrompt(prompt), ns => ns.SendPrompt(prompt));
}
- [Collection("Sequential Server Tests")]
- public class UnicodePromptTests
+ private class TestPrompt : Prompt
{
- [Fact]
- public void TestUnicodePrompt()
- {
- var prompt = new TestPrompt();
- var expected = new UnicodePrompt(prompt).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPrompt(prompt);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
- }
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs
index d7d3beca2..3d839899b 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs
@@ -6,19 +6,38 @@ namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
public class MobilePacketTests
{
+ private static Mobile CreateMobileWithStats(uint serial = 0x1, string name = null)
+ {
+ var m = new Mobile((Serial)serial) { Name = name };
+ m.DefaultMobileInit();
+ m.Str = 50;
+ m.Hits = 100;
+ m.Int = 75;
+ m.Mana = 100;
+ m.Dex = 25;
+ m.Stam = 100;
+
+ return m;
+ }
+
+ private sealed class TestPoison : Poison
+ {
+ public TestPoison(int level) : base(level) => Level = level;
+
+ public override string Name => $"Test Poison {Level}";
+ public override int Level { get; }
+ public override PoisonFamily Family => PoisonFamily.Standard;
+
+ public override Timer ConstructTimer(Mobile m) => null;
+ }
+
[Fact]
public void TestDeathAnimation()
{
var killed = (Serial)0x1;
var corpse = (Serial)0x1000;
- var expected = new DeathAnimation(killed, corpse).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDeathAnimation(killed, corpse);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DeathAnimation(killed, corpse), ns => ns.SendDeathAnimation(killed, corpse));
}
[Fact]
@@ -27,13 +46,7 @@ public class MobilePacketTests
var petSerial = (Serial)0x1;
const bool bonded = true;
- var expected = new BondedStatus(petSerial, bonded).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendBondedStatus(petSerial, bonded);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new BondedStatus(petSerial, bonded), ns => ns.SendBondedStatus(petSerial, bonded));
}
[Theory]
@@ -46,14 +59,11 @@ public class MobilePacketTests
var noto = 10;
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = protocolChanges;
- var expected = new MobileMoving(m, noto, ns.StygianAbyss).Compile();
-
- ns.SendMobileMoving(m, noto);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ ns => new MobileMoving(m, noto, ns.StygianAbyss),
+ ns => ns.SendMobileMoving(m, noto),
+ protocolChanges
+ );
}
[Theory]
@@ -65,13 +75,7 @@ public class MobilePacketTests
var m = new Mobile((Serial)0x1) { Name = name };
m.DefaultMobileInit();
- var expected = new MobileName(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileName(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileName(m), ns => ns.SendMobileName(m));
}
[Theory]
@@ -81,29 +85,10 @@ public class MobilePacketTests
{
var mobile = (Serial)0x1;
- var expected = new MobileAnimation(
- mobile,
- action,
- frameCount,
- repeatCount,
- !reverse,
- repeat,
- delay
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileAnimation(
- mobile,
- action,
- frameCount,
- repeatCount,
- !reverse,
- repeat,
- delay
+ PacketAssert.Sends(
+ new MobileAnimation(mobile, action, frameCount, repeatCount, !reverse, repeat, delay),
+ ns => ns.SendMobileAnimation(mobile, action, frameCount, repeatCount, !reverse, repeat, delay)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Theory]
@@ -113,43 +98,24 @@ public class MobilePacketTests
{
var mobile = (Serial)0x1;
- var expected = new NewMobileAnimation(
- mobile,
- action,
- frameCount,
- delay
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendNewMobileAnimation(
- mobile,
- action,
- frameCount,
- delay
+ PacketAssert.Sends(
+ new NewMobileAnimation(mobile, action, frameCount, delay),
+ ns => ns.SendNewMobileAnimation(mobile, action, frameCount, delay)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
+ // Level -1 means not poisoned. Constructed directly: the poison registry is filled by UOContent.
[Theory]
- [InlineData("None")]
- [InlineData("Lesser")]
- [InlineData("Lethal")]
- public void TestHealthbarPoison(string pName)
+ [InlineData(-1)]
+ [InlineData(0)]
+ [InlineData(4)]
+ public void TestHealthbarPoison(int level)
{
- var p = Poison.GetPoison(pName);
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
- m.Poison = p;
+ m.Poison = level < 0 ? null : new TestPoison(level);
- var expected = new HealthbarPoison(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileHealthbar(m, Healthbar.Poison);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new HealthbarPoison(m), ns => ns.SendMobileHealthbar(m, Healthbar.Poison));
}
[Theory]
@@ -164,13 +130,7 @@ public class MobilePacketTests
m.Blessed = isBlessed;
m.YellowHealthbar = isYellowHealth;
- var expected = new HealthbarYellow(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileHealthbar(m, Healthbar.Yellow);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new HealthbarYellow(m), ns => ns.SendMobileHealthbar(m, Healthbar.Yellow));
}
[Theory]
@@ -178,22 +138,9 @@ public class MobilePacketTests
[InlineData(false)]
public void TestMobileStatusCompact(bool canBeRenamed)
{
- var m = new Mobile((Serial)0x1) { Name = "Random Mobile 1" };
- m.DefaultMobileInit();
- m.Str = 50;
- m.Hits = 100;
- m.Int = 75;
- m.Mana = 100;
- m.Dex = 25;
- m.Stam = 100;
+ var m = CreateMobileWithStats(name: "Random Mobile 1");
- var expected = new MobileStatusCompact(canBeRenamed, m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileStatusCompact(m, canBeRenamed);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileStatusCompact(canBeRenamed, m), ns => ns.SendMobileStatusCompact(m, canBeRenamed));
}
[Theory]
@@ -201,32 +148,14 @@ public class MobilePacketTests
[InlineData(ProtocolChanges.Version502b)]
public void TestMobileStatus(ProtocolChanges changes)
{
- var beholder = new Mobile((Serial)0x1) { Name = "Random Mobile 1" };
- beholder.DefaultMobileInit();
- beholder.Str = 50;
- beholder.Hits = 100;
- beholder.Int = 75;
- beholder.Mana = 100;
- beholder.Dex = 25;
- beholder.Stam = 100;
+ var beholder = CreateMobileWithStats(0x1, "Random Mobile 1");
+ var beheld = CreateMobileWithStats(0x2, "Random Mobile 2");
- var beheld = new Mobile((Serial)0x2) { Name = "Random Mobile 2" };
- beheld.DefaultMobileInit();
- beheld.Str = 50;
- beheld.Hits = 100;
- beheld.Int = 75;
- beheld.Mana = 100;
- beheld.Dex = 25;
- beheld.Stam = 100;
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = changes;
-
- var expected = new MobileStatus(beholder, beheld, ns).Compile();
- ns.SendMobileStatus(beholder, beheld);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ ns => new MobileStatus(beholder, beheld, ns),
+ ns => ns.SendMobileStatus(beholder, beheld),
+ changes
+ );
}
[Theory]
@@ -240,33 +169,32 @@ public class MobilePacketTests
int mobileStatusVersion
)
{
- var expansionInfo = ExpansionInfo.CoreExpansion;
var oldExpansion = Core.Expansion;
+ var expansionInfo = ExpansionInfo.GetInfo(expansion);
var oldVersion = expansionInfo.MobileStatusVersion;
- Core.Expansion = expansion;
- ExpansionInfo.CoreExpansion.MobileStatusVersion = mobileStatusVersion;
- var m = new Mobile((Serial)0x1) { Name = "Random Mobile 1" };
- m.DefaultMobileInit();
- m.Str = 50;
- m.Hits = 100;
- m.Int = 75;
- m.Mana = 100;
- m.Dex = 25;
- m.Stam = 100;
+ try
+ {
+ Core.Expansion = expansion;
+ expansionInfo.MobileStatusVersion = mobileStatusVersion;
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = changes;
- ns.Version = new ClientVersion(version);
- ns.Flags = clientFlags;
+ var m = CreateMobileWithStats(name: "Random Mobile 1");
- var expected = new MobileStatusExtended(m, ns).Compile();
- ns.SendMobileStatus(m, m);
+ using var ns = PacketTestUtilities.CreateTestNetState();
+ ns.ProtocolChanges = changes;
+ ns.Version = new ClientVersion(version);
+ ns.Flags = clientFlags;
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
- Core.Expansion = oldExpansion;
- expansionInfo.MobileStatusVersion = oldVersion;
+ var expected = new MobileStatusExtended(m, ns).Compile();
+ ns.SendMobileStatus(m, m);
+
+ AssertThat.Equal(ns.SendBuffer.GetReadSpan(), expected);
+ }
+ finally
+ {
+ Core.Expansion = oldExpansion;
+ expansionInfo.MobileStatusVersion = oldVersion;
+ }
}
[Theory]
@@ -278,14 +206,7 @@ public class MobilePacketTests
m.DefaultMobileInit();
m.SolidHueOverride = solidHueOverride;
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = changes;
-
- var expected = new MobileUpdate(m, ns.StygianAbyss).Compile();
- ns.SendMobileUpdate(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(ns => new MobileUpdate(m, ns.StygianAbyss), ns => ns.SendMobileUpdate(m), changes);
}
[Theory]
@@ -333,170 +254,82 @@ public class MobilePacketTests
beheld.FacialHairItemID = facialHairItemId;
beheld.FacialHairHue = facialHairHue;
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = changes;
-
- var expected = new MobileIncoming(ns, beholder, beheld).Compile();
- ns.SendMobileIncoming(beholder, beheld);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ ns => new MobileIncoming(ns, beholder, beheld),
+ ns => ns.SendMobileIncoming(beholder, beheld),
+ changes
+ );
}
[Fact]
public void TestMobileHits()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Str = 100;
- m.Hits = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileHits(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileHits(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileHits(m), ns => ns.SendMobileHits(m));
}
[Fact]
public void TestMobileHitsN()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Str = 100;
- m.Hits = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileHitsN(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileHits(m, true);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileHitsN(m), ns => ns.SendMobileHits(m, true));
}
[Fact]
public void TestMobileMana()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Int = 75;
- m.Mana = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileMana(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileMana(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileMana(m), ns => ns.SendMobileMana(m));
}
[Fact]
public void TestMobileManaN()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Int = 75;
- m.Mana = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileManaN(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileMana(m, true);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileManaN(m), ns => ns.SendMobileMana(m, true));
}
[Fact]
public void TestMobileStam()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Dex = 75;
- m.Stam = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileStam(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileStam(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileStam(m), ns => ns.SendMobileStam(m));
}
[Fact]
public void TestMobileStamN()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Dex = 75;
- m.Stam = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileStamN(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileStam(m, true);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileStamN(m), ns => ns.SendMobileStam(m, true));
}
[Fact]
public void TestMobileAttributes()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Str = 50;
- m.Hits = 100;
- m.Int = 75;
- m.Mana = 100;
- m.Dex = 25;
- m.Stam = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileAttributes(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileAttributes(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileAttributes(m), ns => ns.SendMobileAttributes(m));
}
[Fact]
public void TestMobileAttributesN()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Str = 50;
- m.Hits = 100;
- m.Int = 75;
- m.Mana = 100;
- m.Dex = 25;
- m.Stam = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileAttributesN(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileAttributes(m, true);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileAttributesN(m), ns => ns.SendMobileAttributes(m, true));
}
[Fact]
public void TestRemoveEntity()
{
var e = (Serial)0x1000;
- var expected = new RemoveEntity(e).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendRemoveEntity(e);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new RemoveEntity(e), ns => ns.SendRemoveEntity(e));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs
index 330eaf0b5..5f8a27206 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs
@@ -12,26 +12,18 @@ public class MovementPacketTests
[InlineData(2)]
public void TestSpeedControl(byte speedControl)
{
- var expected = new SpeedControl(speedControl).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSpeedControl((SpeedControlSetting)speedControl);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new SpeedControl(speedControl),
+ ns => ns.SendSpeedControl((SpeedControlSetting)speedControl)
+ );
}
[Fact]
public void TestMovePlayer()
{
const Direction d = Direction.Left;
- var expected = new MovePlayer(d).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMovePlayer(d);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MovePlayer(d), ns => ns.SendMovePlayer(d));
}
[Fact]
@@ -42,13 +34,7 @@ public class MovementPacketTests
const byte seq = 100;
- var expected = new MovementRej(seq, m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMovementRej(seq, m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MovementRej(seq, m), ns => ns.SendMovementRej(seq, m));
}
[Fact]
@@ -59,12 +45,6 @@ public class MovementPacketTests
const byte seq = 100;
- var expected = MovementAck.Instantiate(seq, m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMovementAck(seq, m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(MovementAck.Instantiate(seq, m), ns => ns.SendMovementAck(seq, m));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs
index 6d6e5daa3..8bfe17a9e 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs
@@ -8,23 +8,16 @@ namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
public class PlayerPacketTests
{
- [Theory]
- [InlineData(StatLockType.Down, StatLockType.Up, StatLockType.Locked)]
- public void TestStatLockInfo(StatLockType str, StatLockType intel, StatLockType dex)
+ [Fact]
+ public void TestStatLockInfo()
{
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
- m.StrLock = str;
- m.IntLock = intel;
- m.DexLock = dex;
+ m.StrLock = StatLockType.Down;
+ m.IntLock = StatLockType.Up;
+ m.DexLock = StatLockType.Locked;
- var expected = new StatLockInfo(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendStatLockInfo(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new StatLockInfo(m), ns => ns.SendStatLockInfo(m));
}
[Theory]
@@ -33,25 +26,13 @@ public class PlayerPacketTests
[InlineData(200)]
public void TestChangeUpdateRange(int range)
{
- var expected = new ChangeUpdateRange(range).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendChangeUpdateRange((byte)range);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ChangeUpdateRange(range), ns => ns.SendChangeUpdateRange((byte)range));
}
[Fact]
public void TestDeathStatus()
{
- var expected = new DeathStatus(false).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDeathStatus();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DeathStatus(false), ns => ns.SendDeathStatus());
}
[Theory]
@@ -59,13 +40,10 @@ public class PlayerPacketTests
[InlineData(0x1000u, null, null, null)]
public void TestDisplayProfile(uint serial, string header, string body, string footer)
{
- var expected = new DisplayProfile((Serial)serial, header, body, footer).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayProfile((Serial)serial, header, body, footer);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplayProfile((Serial)serial, header, body, footer),
+ ns => ns.SendDisplayProfile((Serial)serial, header, body, footer)
+ );
}
[Theory]
@@ -73,55 +51,31 @@ public class PlayerPacketTests
[InlineData(LRReason.TryToSteal)]
public void TestLiftRej(LRReason reason)
{
- var expected = new LiftRej(reason).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendLiftReject(reason);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new LiftRej(reason), ns => ns.SendLiftReject(reason));
}
[Fact]
public void TestLogoutAck()
{
- var expected = new LogoutAck().Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendLogoutAck();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new LogoutAck(), ns => ns.SendLogoutAck());
}
[Theory]
[InlineData(1, 2, 3)]
[InlineData(4, 5, 6)]
- [InlineData(0x1234, 0x5678, 0x9ABC)]
- public void TestWeather(int type, int density, int temp)
+ [InlineData(0xFF, 0x46, 0xEC)]
+ public void TestWeather(byte type, byte density, byte temp)
{
- var expected = new Weather(type, density, temp).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendWeather((byte)type, (byte)density, (byte)temp);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new Weather(type, density, temp), ns => ns.SendWeather(type, density, temp));
}
- [Theory]
- [InlineData(100, 1000, 1, 0)]
- public void TestServerChange(int x, int y, int z, int mapID)
+ [Fact]
+ public void TestServerChange()
{
- var p = new Point3D(x, y, z);
- var map = Map.Maps[mapID];
- var expected = new ServerChange(p, map).Compile();
+ var p = new Point3D(100, 1000, 1);
+ var map = Map.Maps[0];
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendServerChange(p, map);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ServerChange(p, map), ns => ns.SendServerChange(p, map));
}
[Theory]
@@ -130,13 +84,7 @@ public class PlayerPacketTests
[InlineData(255)]
public void TestSequence(byte num)
{
- var expected = new Sequence(num).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSequence(num);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new Sequence(num), ns => ns.SendSequence(num));
}
[Theory]
@@ -146,37 +94,22 @@ public class PlayerPacketTests
[InlineData("https://www.modernuo.com")]
public void TestLaunchBrowser(string uri)
{
- var expected = new LaunchBrowser(uri).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendLaunchBrowser(uri);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new LaunchBrowser(uri), ns => ns.SendLaunchBrowser(uri));
}
- [Theory]
- [InlineData(0x1000u, 1000, 100, 10, 0x2000u, 1125, 125, 5, 0x384, 1024, 25)]
- public void TestDragEffect(
- uint srcSerial, int srcX, int srcY, int srcZ,
- uint trgSerial, int trgX, int trgY, int trgZ,
- int itemId, int hue, int amount
- )
+ [Fact]
+ public void TestDragEffect()
{
- var src = new Entity((Serial)srcSerial, new Point3D(srcX, srcY, srcZ), null);
- var targ = new Entity((Serial)trgSerial, new Point3D(trgX, trgY, trgZ), null);
+ var src = new Entity((Serial)0x1000, new Point3D(1000, 100, 10), null);
+ var targ = new Entity((Serial)0x2000, new Point3D(1125, 125, 5), null);
+ const int itemId = 0x384;
+ const int hue = 1024;
+ const int amount = 25;
- var expected = new DragEffect(src, targ, itemId, hue, amount).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDragEffect(
- src.Serial, src.Location,
- targ.Serial, targ.Location,
- itemId, hue, amount
+ PacketAssert.Sends(
+ new DragEffect(src, targ, itemId, hue, amount),
+ ns => ns.SendDragEffect(src.Serial, src.Location, targ.Serial, targ.Location, itemId, hue, amount)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Theory]
@@ -184,13 +117,7 @@ public class PlayerPacketTests
[InlineData(2, true)]
public void TestSeasonChange(int season, bool playSound)
{
- var expected = new SeasonChange(season, playSound).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSeasonChange((byte)season, playSound);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new SeasonChange(season, playSound), ns => ns.SendSeasonChange((byte)season, playSound));
}
[Theory]
@@ -199,13 +126,10 @@ public class PlayerPacketTests
[InlineData(0x1024u, "Test Title", true, false)]
public void TestDisplayPaperdoll(uint m, string title, bool warmode, bool canLift)
{
- var expected = new DisplayPaperdoll((Serial)m, title, warmode, canLift).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayPaperdoll((Serial)m, title, warmode, canLift);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplayPaperdoll((Serial)m, title, warmode, canLift),
+ ns => ns.SendDisplayPaperdoll((Serial)m, title, warmode, canLift)
+ );
}
[Theory]
@@ -214,13 +138,7 @@ public class PlayerPacketTests
[InlineData(MusicName.ValoriaShips)]
public void TestPlayMusic(MusicName music)
{
- var expected = new PlayMusic(music).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPlayMusic(music);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new PlayMusic(music), ns => ns.SendPlayMusic(music));
}
[Theory]
@@ -228,41 +146,23 @@ public class PlayerPacketTests
[InlineData(100, 10, "Some more text")]
public void TestScrollMessage(int type, int tip, string text)
{
- var expected = new ScrollMessage(type, tip, text).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendScrollMessage(type, tip ,text);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ScrollMessage(type, tip, text), ns => ns.SendScrollMessage(type, tip, text));
}
- [Theory]
- [InlineData(14, 10, 05)]
- public void TestCurrentTime(int hour, int minute, int second)
+ [Fact]
+ public void TestCurrentTime()
{
- var date = new DateTime(2020, 1, 1, hour, minute, second);
- var expected = new CurrentTime(date).Compile();
+ var date = new DateTime(2020, 1, 1, 14, 10, 5);
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCurrentTime(date);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new CurrentTime(date), ns => ns.SendCurrentTime(date));
}
- [Theory]
- [InlineData(1000, 10, 1)]
- public void TestPathfindMessage(int x, int y, int z)
+ [Fact]
+ public void TestPathfindMessage()
{
- var p = new Point3D(x, y, z);
- var expected = new PathfindMessage(p).Compile();
+ var p = new Point3D(1000, 10, 1);
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPathfindMessage(p);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new PathfindMessage(p), ns => ns.SendPathfindMessage(p));
}
[Theory]
@@ -271,27 +171,17 @@ public class PlayerPacketTests
[InlineData(100)]
public void TestPingAck(byte ping)
{
- var expected = new PingAck(ping).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPingAck(ping);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new PingAck(ping), ns => ns.SendPingAck(ping));
}
- [Theory]
- [InlineData(0xFF01)]
- public void TestDisplayHuePicker(int itemID)
+ [Fact]
+ public void TestDisplayHuePicker()
{
- var huePicker = new HuePicker(itemID);
+ var huePicker = new HuePicker(0xFF01);
- var expected = new DisplayHuePicker(huePicker).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayHuePicker(huePicker.Serial, huePicker.ItemID);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplayHuePicker(huePicker),
+ ns => ns.SendDisplayHuePicker(huePicker.Serial, huePicker.ItemID)
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs
index 6c10b63e9..ca2934b0c 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs
@@ -18,13 +18,10 @@ public class SecureTradePacketTests
var firstCont = new Container(World.NewItem);
var secondCont = new Container(World.NewItem);
- var expected = new DisplaySecureTrade(m, firstCont, secondCont, name).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplaySecureTrade(m, firstCont, secondCont, name);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplaySecureTrade(m, firstCont, secondCont, name),
+ ns => ns.SendDisplaySecureTrade(m, firstCont, secondCont, name)
+ );
}
[Fact]
@@ -32,13 +29,7 @@ public class SecureTradePacketTests
{
var cont = new Container(World.NewItem);
- var expected = new CloseSecureTrade(cont).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCloseSecureTrade(cont);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new CloseSecureTrade(cont), ns => ns.SendCloseSecureTrade(cont));
}
[Theory, InlineData(true, false), InlineData(false, true)]
@@ -50,13 +41,11 @@ public class SecureTradePacketTests
var secondCont = new Container(World.NewItem);
var cont = first ? firstCont : secondCont;
- var expected = new UpdateSecureTrade(cont, first, second).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendUpdateSecureTrade(cont, first, second);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new UpdateSecureTrade(cont, first, second),
+ ns => ns.SendUpdateSecureTrade(cont, first, second)
+ );
}
[Theory]
@@ -65,13 +54,11 @@ public class SecureTradePacketTests
public void TestUpdateGoldSecureTrade(int gold, int plat, TradeFlag flag)
{
var cont = new Container(World.NewItem);
- var expected = new UpdateSecureTrade(cont, flag, gold, plat).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendUpdateSecureTrade(cont, flag, gold, plat);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new UpdateSecureTrade(cont, flag, gold, plat),
+ ns => ns.SendUpdateSecureTrade(cont, flag, gold, plat)
+ );
}
[Fact]
@@ -83,13 +70,7 @@ public class SecureTradePacketTests
var cont = new Container(World.NewItem);
var itemInCont = new Item(World.NewItem) { Parent = cont };
- var expected = new SecureTradeEquip(itemInCont, m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSecureTradeEquip(itemInCont, m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new SecureTradeEquip(itemInCont, m), ns => ns.SendSecureTradeEquip(itemInCont, m));
}
[Fact]
@@ -101,14 +82,10 @@ public class SecureTradePacketTests
var cont = new Container(World.NewItem);
var itemInCont = new Item(World.NewItem) { Parent = cont };
- var expected = new SecureTradeEquip6017(itemInCont, m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
-
- ns.SendSecureTradeEquip(itemInCont, m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new SecureTradeEquip6017(itemInCont, m),
+ ns => ns.SendSecureTradeEquip(itemInCont, m),
+ ProtocolChanges.ContainerGridLines
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/TargetPacketsTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/TargetPacketsTests.cs
index c2d4d29fc..4cc8112b9 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/TargetPacketsTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/TargetPacketsTests.cs
@@ -38,14 +38,7 @@ public class TargetPacketsTests
var p = new Point3D(1000, 100, 10);
MultiTarget t = new TestMultiTarget(multiID, p);
- var expected = new MultiTargetReqHS(t).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.HighSeas;
- ns.SendMultiTargetReq(t);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MultiTargetReqHS(t), ns => ns.SendMultiTargetReq(t), ProtocolChanges.HighSeas);
}
[Fact]
@@ -55,36 +48,20 @@ public class TargetPacketsTests
var p = new Point3D(1000, 100, 10);
MultiTarget t = new TestMultiTarget(multiID, p);
- var expected = new MultiTargetReq(t).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMultiTargetReq(t);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MultiTargetReq(t), ns => ns.SendMultiTargetReq(t));
}
[Fact]
public void TestCancelTarget()
{
- var expected = new CancelTarget().Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCancelTarget();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new CancelTarget(), ns => ns.SendCancelTarget());
}
[Fact]
public void TestTargetReq()
{
var t = new TestTarget(10, true, TargetFlags.Beneficial);
- var expected = new TargetReq(t).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendTargetReq(t);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new TargetReq(t), ns => ns.SendTargetReq(t));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs
index 6262cc243..10f498a20 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs
@@ -22,15 +22,11 @@ public class VendorBuyPacketTests
new("Third Item", cont.Serial, World.NewItem, 30, 10, 0x0F, 0)
};
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = protocolChanges;
-
- var expected = new VendorBuyContent(buyStates, ns.ContainerGridLines).Compile();
-
- ns.SendVendorBuyContent(buyStates);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ ns => new VendorBuyContent(buyStates, ns.ContainerGridLines),
+ ns => ns.SendVendorBuyContent(buyStates),
+ protocolChanges
+ );
}
[Theory]
@@ -41,15 +37,11 @@ public class VendorBuyPacketTests
var vendor = new Mobile((Serial)0x1);
vendor.DefaultMobileInit();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = protocolChanges;
-
- var expected = new DisplayBuyList(vendor.Serial, ns.HighSeas).Compile();
-
- ns.SendDisplayBuyList(vendor.Serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ ns => new DisplayBuyList(vendor.Serial, ns.HighSeas),
+ ns => ns.SendDisplayBuyList(vendor.Serial),
+ protocolChanges
+ );
}
[Fact]
@@ -67,13 +59,7 @@ public class VendorBuyPacketTests
new("Third Item", cont.Serial, World.NewItem, 30, 10, 0x0F, 0)
};
- var expected = new VendorBuyList(vendor, buyStates).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendVendorBuyList(vendor, buyStates);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new VendorBuyList(vendor, buyStates), ns => ns.SendVendorBuyList(vendor, buyStates));
}
[Fact]
@@ -82,12 +68,6 @@ public class VendorBuyPacketTests
var vendor = new Mobile((Serial)0x1);
vendor.DefaultMobileInit();
- var expected = new EndVendorBuy(vendor.Serial).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendEndVendorBuy(vendor.Serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new EndVendorBuy(vendor.Serial), ns => ns.SendEndVendorBuy(vendor.Serial));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs
index 154223349..0b911e6bb 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs
@@ -25,13 +25,10 @@ public class VendorSellPacketTests
new(item3, 1, "Item 3")
};
- var expected = new VendorSellList(vendor, sellStates.ToList()).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendVendorSellList(vendor.Serial, sellStates);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new VendorSellList(vendor, sellStates.ToList()),
+ ns => ns.SendVendorSellList(vendor.Serial, sellStates)
+ );
}
[Fact]
@@ -40,12 +37,6 @@ public class VendorSellPacketTests
var vendor = new Mobile((Serial)0x1024u);
vendor.DefaultMobileInit();
- var expected = new EndVendorBuy(vendor.Serial).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendEndVendorSell(vendor.Serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new EndVendorSell(vendor), ns => ns.SendEndVendorSell(vendor.Serial));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs
index 3a105d1a4..2d70896f4 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs
@@ -15,13 +15,10 @@ public class VirtualHairPacketTests
m.HairHue = 0x1000;
m.HairItemID = 0x2000;
- var expected = new HairEquipUpdate(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendHairEquipUpdatePacket(m, (uint)m.HairSerial, m.HairItemID, m.HairHue, Layer.Hair);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new HairEquipUpdate(m),
+ ns => ns.SendHairEquipUpdatePacket(m, (uint)m.HairSerial, m.HairItemID, m.HairHue, Layer.Hair)
+ );
}
[Fact]
@@ -30,13 +27,7 @@ public class VirtualHairPacketTests
var m = new Mobile((Serial)0x1024u);
m.DefaultMobileInit();
- var expected = new RemoveHair(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendRemoveHairPacket((uint)m.HairSerial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new RemoveHair(m), ns => ns.SendRemoveHairPacket((uint)m.HairSerial));
}
[Fact]
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs
index 135a6e698..ac7ebbf59 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs
@@ -16,22 +16,6 @@ public sealed class HairEquipUpdate : Packet
}
}
-public sealed class FacialHairEquipUpdate : Packet
-{
- public FacialHairEquipUpdate(Mobile parent)
- : base(0x2E, 15)
- {
- var hue = parent.SolidHueOverride >= 0 ? parent.SolidHueOverride : parent.FacialHairHue;
-
- Stream.Write(parent.FacialHairSerial);
- Stream.Write((short)parent.FacialHairItemID);
- Stream.Write((byte)0);
- Stream.Write((byte)Layer.FacialHair);
- Stream.Write(parent.Serial);
- Stream.Write((short)hue);
- }
-}
-
public sealed class RemoveHair : Packet
{
public RemoveHair(Mobile parent)
@@ -40,12 +24,3 @@ public sealed class RemoveHair : Packet
Stream.Write(parent.HairSerial);
}
}
-
-public sealed class RemoveFacialHair : Packet
-{
- public RemoveFacialHair(Mobile parent)
- : base(0x1D, 5)
- {
- Stream.Write(parent.FacialHairSerial);
- }
-}
diff --git a/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListHashTests.cs b/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListHashTests.cs
index 19a8704b7..6dbef52b1 100644
--- a/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListHashTests.cs
+++ b/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListHashTests.cs
@@ -102,36 +102,31 @@ public class ObjectPropertyListHashTests
// 6- and 8-byte blocks take xxHash3's short-input paths. They still avalanche across all 26
// kept bits, so a counter ticking down never repeats the revision it just had.
[Fact]
- public void ShortNumericArguments_ConsecutiveValuesDiffer()
+ public void ShortPropertyBlocks_ConsecutiveValuesDifferAndStayWellDistributed()
{
- var previous = BuildHash((1060584, "0"));
-
- for (var charges = 1; charges < 20000; charges++)
- {
- var current = BuildHash((1060584, charges.ToString()));
- Assert.NotEqual(previous, current);
- previous = current;
- }
- }
-
- [Fact]
- public void SmallPropertyBlocks_StayWellDistributed()
- {
- const int count = 20000;
+ const int count = 5000;
var withArgument = new HashSet();
var withoutArgument = new HashSet();
+ var previous = 0;
for (var i = 0; i < count; i++)
{
- withArgument.Add(BuildHash((1060584, i.ToString())));
+ var current = BuildHash((1060584, i.ToString()));
+ if (i > 0)
+ {
+ Assert.NotEqual(previous, current);
+ }
+
+ previous = current;
+ withArgument.Add(current);
withoutArgument.Add(BuildHash((1060000 + i, null)));
}
- // Birthday expects ~3 collisions over a 26-bit space; allow an order of magnitude so the
- // bound holds for any seed. A hash that stopped mixing collapses far past it.
- Assert.True(withArgument.Count >= count - 30, $"8-byte blocks: {withArgument.Count}/{count}");
- Assert.True(withoutArgument.Count >= count - 30, $"6-byte blocks: {withoutArgument.Count}/{count}");
+ // Birthday expects ~0.2 collisions over a 26-bit space; allow a few so the bound holds for
+ // any seed. A hash that stopped mixing collapses far past it.
+ Assert.True(withArgument.Count >= count - 5, $"8-byte blocks: {withArgument.Count}/{count}");
+ Assert.True(withoutArgument.Count >= count - 5, $"6-byte blocks: {withoutArgument.Count}/{count}");
}
[Fact]
diff --git a/Projects/Server.Tests/Tests/Serialization/AnchoredTimeTests.cs b/Projects/Server.Tests/Tests/Serialization/AnchoredTimeTests.cs
index a99b9850c..74edf09c3 100644
--- a/Projects/Server.Tests/Tests/Serialization/AnchoredTimeTests.cs
+++ b/Projects/Server.Tests/Tests/Serialization/AnchoredTimeTests.cs
@@ -1,6 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.IO;
using Xunit;
namespace Server.Tests;
@@ -107,83 +105,48 @@ public class AnchoredTimePersistenceTests
[Fact]
public void SaveStartAnchor_RebasesAnchoredTimestampsAtLoad()
{
- var previousAssemblies = AssemblyHandler.Assemblies;
- AssemblyHandler.Assemblies = [.. previousAssemblies ?? [], typeof(AnchoredEntity).Assembly];
+ // The scope also restores World.SaveStartTime, which this test backdates.
+ using var pipeline = new PersistencePipelineScope(
+ 2,
+ installWorkers: true,
+ visibleAssembly: typeof(AnchoredEntity).Assembly,
+ directoryPrefix: "muo-anchored"
+ );
+ var source = pipeline.Source;
+ var dir = pipeline.Directory;
- var source = new SerializationChunkSource();
- var workers = new SerializationThreadWorker[2];
- for (var i = 0; i < workers.Length; i++)
- {
- workers[i] = new SerializationThreadWorker(i, source);
- workers[i].AllocateHeap();
- }
+ var persistence = pipeline.Track(new AnchoredPersistence(2100));
- var previousWorkers = World._threadWorkers;
- World._threadWorkers = workers;
+ var lastRested = Core.Now - TimeSpan.FromMinutes(10);
+ var serial = (Serial)1u;
+ persistence.EntitiesBySerial[serial] = new AnchoredEntity(serial) { LastRested = lastRested };
+ persistence.RegisterType(typeof(AnchoredEntity));
- var previousSaveStart = World.SaveStartTime;
+ // Pretend the save started two hours ago, as if the server had been down since.
+ var downtime = TimeSpan.FromHours(2);
+ World.SaveStartTime = Core.Now - downtime;
- var persistence = new AnchoredPersistence(2100);
- AnchoredPersistence loaded = null;
+ pipeline.WakeWorkers();
- var dir = Path.Combine(Path.GetTempPath(), $"muo-anchored-{Guid.NewGuid():N}");
- Directory.CreateDirectory(dir);
+ source.SetOwner(persistence);
+ Assert.True(persistence.TrySnapshotEntries(out var slotCount));
+ source.PushSlotRanges(persistence, slotCount);
- try
- {
- var lastRested = Core.Now - TimeSpan.FromMinutes(10);
- var serial = (Serial)1u;
- persistence.EntitiesBySerial[serial] = new AnchoredEntity(serial) { LastRested = lastRested };
- persistence.RegisterType(typeof(AnchoredEntity));
+ pipeline.FlushAndSleepWorkers();
- // Pretend the save started two hours ago, as if the server had been down since.
- var downtime = TimeSpan.FromHours(2);
- World.SaveStartTime = Core.Now - downtime;
+ persistence.WriteSnapshot(dir);
+ persistence.PostWorldSave();
- foreach (var worker in workers)
- {
- worker.Wake();
- }
+ var loaded = pipeline.Track(new AnchoredPersistence(2101));
+ loaded.DeserializeIndexes(dir, null);
+ loaded.Deserialize(dir, null);
- source.SetOwner(persistence);
- Assert.True(persistence.TrySnapshotEntries(out var slotCount));
- source.PushSlotRanges(persistence, slotCount);
+ var entity = loaded.EntitiesBySerial[serial];
+ var expected = lastRested + downtime;
- source.Flush();
- foreach (var worker in workers)
- {
- worker.Sleep();
- }
-
- persistence.WriteSnapshot(dir);
- persistence.PostWorldSave();
-
- loaded = new AnchoredPersistence(2101);
- loaded.DeserializeIndexes(dir, null);
- loaded.Deserialize(dir, null);
-
- var entity = loaded.EntitiesBySerial[serial];
- var expected = lastRested + downtime;
-
- Assert.True(
- (entity.LastRested - expected).Duration() <= TimeSpan.FromSeconds(30),
- $"Anchored timestamp must re-base by the downtime; expected ~{expected}, got {entity.LastRested}."
- );
- }
- finally
- {
- World.SaveStartTime = previousSaveStart;
- persistence.Unregister();
- loaded?.Unregister();
-
- foreach (var worker in workers)
- {
- worker.Exit();
- }
-
- World._threadWorkers = previousWorkers;
- AssemblyHandler.Assemblies = previousAssemblies;
- Directory.Delete(dir, true);
- }
+ Assert.True(
+ (entity.LastRested - expected).Duration() <= TimeSpan.FromSeconds(30),
+ $"Anchored timestamp must re-base by the downtime; expected ~{expected}, got {entity.LastRested}."
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs b/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs
index 48ba16e55..c8c4522aa 100644
--- a/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs
+++ b/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs
@@ -1,6 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.IO;
using Xunit;
namespace Server.Tests;
@@ -72,103 +70,68 @@ public class GenericEntityPersistenceRoundTripTests
[Fact]
public void SnapshotRoundTripsThroughWorkersAndSegmentLogs()
{
- // The loader resolves types by hash through AssemblyHandler; make this test
- // assembly visible for the duration of the test.
- var previousAssemblies = AssemblyHandler.Assemblies;
- AssemblyHandler.Assemblies = [.. previousAssemblies ?? [], typeof(RoundTripEntity).Assembly];
+ // The loader resolves types by hash through AssemblyHandler, so this test assembly must be visible.
+ using var pipeline = new PersistencePipelineScope(
+ 3,
+ installWorkers: true,
+ visibleAssembly: typeof(RoundTripEntity).Assembly,
+ directoryPrefix: "muo-roundtrip"
+ );
+ var source = pipeline.Source;
+ var dir = pipeline.Directory;
- var source = new SerializationChunkSource();
- var workers = new SerializationThreadWorker[3];
- for (var i = 0; i < workers.Length; i++)
+ var persistence = pipeline.Track(new RoundTripPersistence(2000));
+
+ const int entityCount = 25_000;
+ var rng = new System.Random(0x5EED);
+
+ for (var i = 1; i <= entityCount; i++)
{
- workers[i] = new SerializationThreadWorker(i, source);
- workers[i].AllocateHeap();
+ var serial = (Serial)(uint)i;
+ persistence.EntitiesBySerial[serial] = new RoundTripEntity(serial)
+ {
+ Value = rng.Next(),
+ Name = i % 5 == 0 ? null : $"entity-{i}"
+ };
}
- var previousWorkers = World._threadWorkers;
- World._threadWorkers = workers;
+ persistence.RegisterType(typeof(RoundTripEntity));
- var persistence = new RoundTripPersistence(2000);
- RoundTripPersistence loaded = null;
+ // Freeze: what Persistence.SerializeAll + GenericEntityPersistence.Serialize do,
+ // against this test's chunk source instead of the world's.
+ pipeline.WakeWorkers();
- var dir = Path.Combine(Path.GetTempPath(), $"muo-roundtrip-{Guid.NewGuid():N}");
- Directory.CreateDirectory(dir);
+ source.SetOwner(persistence);
+ source.PushSingle(persistence);
+ Assert.True(persistence.TrySnapshotEntries(out var slotCount));
+ source.PushSlotRanges(persistence, slotCount);
- try
+ pipeline.FlushAndSleepWorkers();
+
+ // Background write phase: snapshot from the segment logs. v4 embeds the type
+ // table in the idx, so no SerializedTypes.db is produced or needed.
+ persistence.WriteSnapshot(dir);
+
+ persistence.PostWorldSave(); // releases the entries snapshot
+
+ // Load into a fresh persistence, like a server boot would.
+ var loaded = pipeline.Track(new RoundTripPersistence(2001));
+ loaded.DeserializeIndexes(dir, null);
+ loaded.Deserialize(dir, null);
+
+ Assert.True(loaded.SelfPayloadDeserialized);
+ Assert.Equal(persistence.EntitiesBySerial.Count, loaded.EntitiesBySerial.Count);
+
+ foreach (var (serial, original) in persistence.EntitiesBySerial)
{
- const int entityCount = 25_000;
- var rng = new System.Random(0x5EED);
-
- for (var i = 1; i <= entityCount; i++)
- {
- var serial = (Serial)(uint)i;
- persistence.EntitiesBySerial[serial] = new RoundTripEntity(serial)
- {
- Value = rng.Next(),
- Name = i % 5 == 0 ? null : $"entity-{i}"
- };
- }
-
- persistence.RegisterType(typeof(RoundTripEntity));
-
- // Freeze: what Persistence.SerializeAll + GenericEntityPersistence.Serialize do,
- // against this test's chunk source instead of the world's.
- foreach (var worker in workers)
- {
- worker.Wake();
- }
-
- source.SetOwner(persistence);
- source.PushSingle(persistence);
- Assert.True(persistence.TrySnapshotEntries(out var slotCount));
- source.PushSlotRanges(persistence, slotCount);
-
- // Mirrors World.PauseSerializationThreads
- source.Flush();
- foreach (var worker in workers)
- {
- worker.Sleep();
- }
-
- // Background write phase: snapshot from the segment logs. v4 embeds the type
- // table in the idx, so no SerializedTypes.db is produced or needed.
- persistence.WriteSnapshot(dir);
-
- persistence.PostWorldSave(); // releases the entries snapshot
-
- // Load into a fresh persistence, like a server boot would.
- loaded = new RoundTripPersistence(2001);
- loaded.DeserializeIndexes(dir, null);
- loaded.Deserialize(dir, null);
-
- Assert.True(loaded.SelfPayloadDeserialized);
- Assert.Equal(persistence.EntitiesBySerial.Count, loaded.EntitiesBySerial.Count);
-
- foreach (var (serial, original) in persistence.EntitiesBySerial)
- {
- var entity = loaded.EntitiesBySerial[serial];
- Assert.Equal(original.Value, entity.Value);
- Assert.Equal(original.Name, entity.Name);
- Assert.Equal(original.Created.Ticks, entity.Created.Ticks);
- }
-
- // Loading must hydrate the type table so the next save can write indexes.
- Assert.True(loaded.TryGetTypeIndex(typeof(RoundTripEntity), out _));
+ var entity = loaded.EntitiesBySerial[serial];
+ Assert.Equal(original.Value, entity.Value);
+ Assert.Equal(original.Name, entity.Name);
+ Assert.Equal(original.Created.Ticks, entity.Created.Ticks);
}
- finally
- {
- persistence.Unregister();
- loaded?.Unregister();
- foreach (var worker in workers)
- {
- worker.Exit();
- }
-
- World._threadWorkers = previousWorkers;
- AssemblyHandler.Assemblies = previousAssemblies;
- Directory.Delete(dir, true);
- }
+ // Loading must hydrate the type table so the next save can write indexes.
+ Assert.True(loaded.TryGetTypeIndex(typeof(RoundTripEntity), out _));
}
///
@@ -179,70 +142,44 @@ public class GenericEntityPersistenceRoundTripTests
[Fact]
public void SerializerException_IsRecordedOnTheWorker_AndTheDrainCompletes()
{
- var source = new SerializationChunkSource();
- var workers = new SerializationThreadWorker[2];
- for (var i = 0; i < workers.Length; i++)
+ using var pipeline = new PersistencePipelineScope(2);
+ var source = pipeline.Source;
+ var workers = pipeline.Workers;
+
+ var persistence = pipeline.Track(new RoundTripPersistence(2002));
+
+ for (var i = 1; i <= 100; i++)
{
- workers[i] = new SerializationThreadWorker(i, source);
- workers[i].AllocateHeap();
+ var serial = (Serial)(uint)i;
+ persistence.EntitiesBySerial[serial] = i == 50
+ ? new ThrowingRoundTripEntity(serial)
+ : new RoundTripEntity(serial) { Value = i, Name = $"entity-{i}" };
}
- var persistence = new RoundTripPersistence(2002);
+ pipeline.WakeWorkers();
- try
+ source.SetOwner(persistence);
+ Assert.True(persistence.TrySnapshotEntries(out var slotCount));
+ source.PushSlotRanges(persistence, slotCount);
+
+ pipeline.FlushAndSleepWorkers();
+
+ Exception error = null;
+ foreach (var worker in workers)
{
- for (var i = 1; i <= 100; i++)
- {
- var serial = (Serial)(uint)i;
- persistence.EntitiesBySerial[serial] = i == 50
- ? new ThrowingRoundTripEntity(serial)
- : new RoundTripEntity(serial) { Value = i, Name = $"entity-{i}" };
- }
-
- foreach (var worker in workers)
- {
- worker.Wake();
- }
-
- source.SetOwner(persistence);
- Assert.True(persistence.TrySnapshotEntries(out var slotCount));
- source.PushSlotRanges(persistence, slotCount);
- source.Flush();
-
- foreach (var worker in workers)
- {
- worker.Sleep();
- }
-
- Exception error = null;
- foreach (var worker in workers)
- {
- error ??= worker.Error;
- }
-
- Assert.IsType(error);
- persistence.PostWorldSave();
-
- // The next drain starts clean.
- foreach (var worker in workers)
- {
- worker.Wake();
- }
-
- foreach (var worker in workers)
- {
- worker.Sleep();
- Assert.Null(worker.Error);
- }
+ error ??= worker.Error;
}
- finally
- {
- persistence.Unregister();
- foreach (var worker in workers)
- {
- worker.Exit();
- }
+ Assert.IsType(error);
+ persistence.PostWorldSave();
+
+ // The next drain starts clean.
+ pipeline.WakeWorkers();
+
+ foreach (var worker in workers)
+ {
+ worker.Sleep();
+ Assert.Null(worker.Error);
}
}
@@ -253,60 +190,28 @@ public class GenericEntityPersistenceRoundTripTests
[Fact]
public void WriteSnapshot_FailsTheSave_InsteadOfDroppingASegment()
{
- var source = new SerializationChunkSource();
- var workers = new SerializationThreadWorker[2];
- for (var i = 0; i < workers.Length; i++)
+ using var pipeline = new PersistencePipelineScope(2, installWorkers: true, directoryPrefix: "muo-segmentfail");
+ var source = pipeline.Source;
+
+ var persistence = pipeline.Track(new RoundTripPersistence(2003));
+
+ for (var i = 1; i <= 100; i++)
{
- workers[i] = new SerializationThreadWorker(i, source);
- workers[i].AllocateHeap();
+ var serial = (Serial)(uint)i;
+ persistence.EntitiesBySerial[serial] = new RoundTripEntity(serial) { Value = i, Name = $"entity-{i}" };
}
- var previousWorkers = World._threadWorkers;
- World._threadWorkers = workers;
+ // Deliberately not registered: the writer cannot index the type.
- var persistence = new RoundTripPersistence(2003);
- var dir = Path.Combine(Path.GetTempPath(), $"muo-segmentfail-{Guid.NewGuid():N}");
- Directory.CreateDirectory(dir);
+ pipeline.WakeWorkers();
- try
- {
- for (var i = 1; i <= 100; i++)
- {
- var serial = (Serial)(uint)i;
- persistence.EntitiesBySerial[serial] = new RoundTripEntity(serial) { Value = i, Name = $"entity-{i}" };
- }
+ source.SetOwner(persistence);
+ Assert.True(persistence.TrySnapshotEntries(out var slotCount));
+ source.PushSlotRanges(persistence, slotCount);
- // Deliberately not registered: the writer cannot index the type.
+ pipeline.FlushAndSleepWorkers();
- foreach (var worker in workers)
- {
- worker.Wake();
- }
-
- source.SetOwner(persistence);
- Assert.True(persistence.TrySnapshotEntries(out var slotCount));
- source.PushSlotRanges(persistence, slotCount);
- source.Flush();
-
- foreach (var worker in workers)
- {
- worker.Sleep();
- }
-
- Assert.Throws(() => persistence.WriteSnapshot(dir));
- persistence.PostWorldSave();
- }
- finally
- {
- persistence.Unregister();
-
- foreach (var worker in workers)
- {
- worker.Exit();
- }
-
- World._threadWorkers = previousWorkers;
- Directory.Delete(dir, true);
- }
+ Assert.Throws(() => persistence.WriteSnapshot(pipeline.Directory));
+ persistence.PostWorldSave();
}
}
diff --git a/Projects/Server.Tests/Tests/Serialization/SerializationChunkSourceTests.cs b/Projects/Server.Tests/Tests/Serialization/SerializationChunkSourceTests.cs
index 69387f4e6..24fea88a7 100644
--- a/Projects/Server.Tests/Tests/Serialization/SerializationChunkSourceTests.cs
+++ b/Projects/Server.Tests/Tests/Serialization/SerializationChunkSourceTests.cs
@@ -199,114 +199,91 @@ public class SerializationChunkSourceTests
[Fact]
public void WorkersDrainAllEntitiesAndLogSegments()
{
- var source = new SerializationChunkSource();
- var owner = new TestPersistence { PayloadSize = 512 * 1024 };
- var workers = new SerializationThreadWorker[2];
- for (var i = 0; i < workers.Length; i++)
+ using var pipeline = new PersistencePipelineScope(2);
+ var source = pipeline.Source;
+ var workers = pipeline.Workers;
+ var owner = pipeline.Track(new TestPersistence { PayloadSize = 512 * 1024 });
+
+ var entities = new List();
+ for (var i = 0; i < 10_000; i++)
{
- workers[i] = new SerializationThreadWorker(i, source);
- workers[i].AllocateHeap();
+ entities.Add(new TestEntity { PayloadSize = 16 + i % 64 });
}
- try
+ pipeline.WakeWorkers();
+
+ // A large payload published as a dedicated single chunk (a persistence
+ // self-payload), interleaved with the bare entity stream.
+ source.SetOwner(owner);
+ for (var i = 0; i < entities.Count; i++)
{
- var entities = new List();
- for (var i = 0; i < 10_000; i++)
+ if (i == 5000)
{
- entities.Add(new TestEntity { PayloadSize = 16 + i % 64 });
+ source.PushSingle(owner);
}
- foreach (var worker in workers)
- {
- worker.Wake();
- }
+ source.Push(entities[i]);
+ }
- // A large payload published as a dedicated single chunk (a persistence
- // self-payload), interleaved with the bare entity stream.
- source.SetOwner(owner);
- for (var i = 0; i < entities.Count; i++)
+ pipeline.FlushAndSleepWorkers();
+
+ long totalEntities = 0;
+ long totalBytes = 0;
+ foreach (var worker in workers)
+ {
+ totalEntities += worker.EntitiesSerialized;
+ totalBytes += worker.BytesSerialized;
+ }
+
+ Assert.Equal(entities.Count + 1, totalEntities);
+
+ // The self-payload recorded its placement on the persistence itself.
+ var (selfThread, selfPosition, selfLength) = owner.Placement;
+ Assert.Equal(owner.PayloadSize, selfLength);
+ Assert.InRange(selfThread, (byte)0, (byte)(workers.Length - 1));
+ var selfHeap = workers[selfThread].GetHeap(selfPosition, selfLength);
+ Assert.Equal(0, selfHeap[0]);
+ Assert.Equal((selfLength - 1) & 0xFF, selfHeap[^1]);
+
+ // Every entity appears exactly once in the worker segment logs, with a
+ // consistent span on that worker's heap: positions are implicit (contiguous
+ // writes), identity comes from the buffer-entities log.
+ var seen = new HashSet();
+ long expectedBytes = owner.PayloadSize;
+ foreach (var e in entities)
+ {
+ expectedBytes += e.PayloadSize;
+ }
+
+ foreach (var worker in workers)
+ {
+ var lengths = worker.Lengths;
+ var bufferEntities = worker.BufferEntities;
+
+ foreach (var segment in worker.Segments)
{
- if (i == 5000)
+ Assert.Same(owner, segment.Owner);
+ Assert.Equal(-1, segment.SlotOffset);
+
+ var heapPos = (int)segment.HeapStart;
+ for (var i = 0; i < segment.RecordCount; i++)
{
- source.PushSingle(owner);
- }
+ var entity = (TestEntity)bufferEntities[segment.EntitiesStart + i];
+ var length = lengths[segment.LengthsStart + i];
- source.Push(entities[i]);
- }
+ Assert.True(seen.Add(entity));
+ Assert.Equal(entity.PayloadSize, length);
- // Mirrors World.PauseSerializationThreads
- source.Flush();
- foreach (var worker in workers)
- {
- worker.Sleep();
- }
+ var heap = workers[Array.IndexOf(workers, worker)].GetHeap(heapPos, length);
+ Assert.Equal(0, heap[0]);
+ Assert.Equal((length - 1) & 0xFF, heap[^1]);
- long totalEntities = 0;
- long totalBytes = 0;
- foreach (var worker in workers)
- {
- totalEntities += worker.EntitiesSerialized;
- totalBytes += worker.BytesSerialized;
- }
-
- Assert.Equal(entities.Count + 1, totalEntities);
-
- // The self-payload recorded its placement on the persistence itself.
- var (selfThread, selfPosition, selfLength) = owner.Placement;
- Assert.Equal(owner.PayloadSize, selfLength);
- Assert.InRange(selfThread, (byte)0, (byte)(workers.Length - 1));
- var selfHeap = workers[selfThread].GetHeap(selfPosition, selfLength);
- Assert.Equal(0, selfHeap[0]);
- Assert.Equal((selfLength - 1) & 0xFF, selfHeap[^1]);
-
- // Every entity appears exactly once in the worker segment logs, with a
- // consistent span on that worker's heap: positions are implicit (contiguous
- // writes), identity comes from the buffer-entities log.
- var seen = new HashSet();
- long expectedBytes = owner.PayloadSize;
- foreach (var e in entities)
- {
- expectedBytes += e.PayloadSize;
- }
-
- foreach (var worker in workers)
- {
- var lengths = worker.Lengths;
- var bufferEntities = worker.BufferEntities;
-
- foreach (var segment in worker.Segments)
- {
- Assert.Same(owner, segment.Owner);
- Assert.Equal(-1, segment.SlotOffset);
-
- var heapPos = (int)segment.HeapStart;
- for (var i = 0; i < segment.RecordCount; i++)
- {
- var entity = (TestEntity)bufferEntities[segment.EntitiesStart + i];
- var length = lengths[segment.LengthsStart + i];
-
- Assert.True(seen.Add(entity));
- Assert.Equal(entity.PayloadSize, length);
-
- var heap = workers[Array.IndexOf(workers, worker)].GetHeap(heapPos, length);
- Assert.Equal(0, heap[0]);
- Assert.Equal((length - 1) & 0xFF, heap[^1]);
-
- heapPos += length;
- }
+ heapPos += length;
}
}
+ }
- Assert.Equal(entities.Count, seen.Count);
- Assert.Equal(expectedBytes, totalBytes);
- }
- finally
- {
- owner.Unregister();
- foreach (var worker in workers)
- {
- worker.Exit();
- }
- }
+ Assert.Equal(entities.Count, seen.Count);
+ Assert.Equal(expectedBytes, totalBytes);
}
}
diff --git a/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs b/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs
index 6c841f9f8..376fd661e 100644
--- a/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs
+++ b/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs
@@ -1,3 +1,4 @@
+using System.Reflection;
using Xunit;
namespace Server.Tests;
@@ -5,20 +6,38 @@ namespace Server.Tests;
[Collection("Sequential Server Tests")]
public class VirtualSerialTests
{
+ // Walking up from ResetVirtualSerial takes ~17.9 million draws; start just short of the wrap.
+ private const uint DrawsBeforeMax = 1000;
+
+ private static readonly FieldInfo _nextVirtualSerial =
+ typeof(World).GetField("_nextVirtualSerial", BindingFlags.NonPublic | BindingFlags.Static);
+
[Fact]
public void TestNewVirtualGetsAndRollover()
{
- // Acquire virtual serials until we hit the max
- var lastSerial = World.NewVirtual;
- do
- {
- var virtualSerial = World.NewVirtual;
- Assert.Equal(lastSerial + 1, virtualSerial);
- lastSerial = (Serial) (uint) virtualSerial;
- } while (lastSerial != World.MaxVirtualSerial);
+ Assert.NotNull(_nextVirtualSerial);
- // Next one should be MaxItemSerial (the first VirtualSerial) due to rollover
- var nextSerial = World.NewVirtual;
- Assert.Equal(nextSerial, (Serial)World.ResetVirtualSerial);
+ var saved = (uint)_nextVirtualSerial.GetValue(null);
+ _nextVirtualSerial.SetValue(null, World.MaxVirtualSerial - DrawsBeforeMax);
+
+ try
+ {
+ // Acquire virtual serials until we hit the max
+ var lastSerial = World.NewVirtual;
+ do
+ {
+ var virtualSerial = World.NewVirtual;
+ Assert.Equal(lastSerial + 1, virtualSerial);
+ lastSerial = (Serial)(uint)virtualSerial;
+ } while (lastSerial != World.MaxVirtualSerial);
+
+ // Next one should be MaxItemSerial (the first VirtualSerial) due to rollover
+ var nextSerial = World.NewVirtual;
+ Assert.Equal(nextSerial, (Serial)World.ResetVirtualSerial);
+ }
+ finally
+ {
+ _nextVirtualSerial.SetValue(null, saved);
+ }
}
}
diff --git a/Projects/UOContent.Tests/Helpers/AiTestStubs.cs b/Projects/UOContent.Tests/Helpers/AiTestStubs.cs
new file mode 100644
index 000000000..9d954c062
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/AiTestStubs.cs
@@ -0,0 +1,42 @@
+using Server.Mobiles;
+
+namespace Server.Tests;
+
+///
+/// A plain : walker, target or bystander. Deriving straight from Mobile rather
+/// than BaseCreature is the point: MovementImpl sees no creature capabilities (no swim, no fly, no
+/// door-opening), which is exactly the walker the step cache bakes for.
+///
+public sealed class WalkerStub : Mobile
+{
+ public WalkerStub() => Body = 0xC9;
+}
+
+///
+/// A BaseCreature built through the Serial constructor, which skips the NPCSpeeds lookup and
+/// leaves AIObject unset: call and attach an AI by hand.
+///
+public sealed class BareCreatureStub : BaseCreature
+{
+ public BareCreatureStub(Serial serial) : base(serial) => Body = 0xC9;
+}
+
+public static class TestTiles
+{
+ ///
+ /// The first item ID whose tile data is an impassable surface, so a test can wall a cell off
+ /// without pinning a specific graphic. 0 when tile data is not loaded.
+ ///
+ public static ushort FirstImpassableItemId()
+ {
+ for (ushort id = 1; id < TileData.MaxItemValue; id++)
+ {
+ if (TileData.ItemTable[id].ImpassableSurface)
+ {
+ return id;
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/Projects/UOContent.Tests/Helpers/CommandTestHarness.cs b/Projects/UOContent.Tests/Helpers/CommandTestHarness.cs
new file mode 100644
index 000000000..61ee1f6b3
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/CommandTestHarness.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using Server.Commands;
+using Server.Items;
+
+namespace Server.Tests;
+
+///
+/// Shared setup for the generic-command compiler tests: teleporters to run conditions, sorts and
+/// distincts against, deleted on dispose, and property bindings resolved for reading.
+///
+internal sealed class CommandTestHarness : IDisposable
+{
+ private readonly List- _items = [];
+
+ public T Track(T item) where T : Item
+ {
+ _items.Add(item);
+ return item;
+ }
+
+ public SkillTeleporter Teleporter(TextDefinition message) => Track(new SkillTeleporter { Message = message });
+
+ public static Property Bind(Type type, string binding)
+ {
+ var prop = new Property(binding);
+ prop.BindTo(type, PropertyAccess.Read);
+ return prop;
+ }
+
+ public void Dispose()
+ {
+ for (var i = 0; i < _items.Count; i++)
+ {
+ _items[i].Delete();
+ }
+
+ _items.Clear();
+ }
+}
diff --git a/Projects/UOContent.Tests/Helpers/MobileTestHarness.cs b/Projects/UOContent.Tests/Helpers/MobileTestHarness.cs
new file mode 100644
index 000000000..30b86f6d6
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/MobileTestHarness.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using Server.Mobiles;
+
+namespace Server.Tests;
+
+///
+/// Owns the mobiles and items a test creates and deletes them on dispose, so a failed assertion
+/// cannot leave entities registered in the shared world. Hold one per test class instance and
+/// dispose it from the class's .
+///
+internal sealed class MobileTestHarness : IDisposable
+{
+ private readonly List _mobiles = [];
+ private readonly List
- _items = [];
+
+ public T Track(T mobile) where T : Mobile
+ {
+ _mobiles.Add(mobile);
+ return mobile;
+ }
+
+ public T TrackItem(T item) where T : Item
+ {
+ _items.Add(item);
+ return item;
+ }
+
+ ///
+ /// A player registered in the world table, so serialized references to it resolve on load.
+ ///
+ public PlayerMobile NewPlayer()
+ {
+ var pm = new PlayerMobile(World.NewMobile);
+ pm.DefaultMobileInit();
+ World.AddEntity(pm);
+ return Track(pm);
+ }
+
+ public TestCreature NewCreature() => Track(new TestCreature());
+
+ public void Dispose()
+ {
+ for (var i = 0; i < _mobiles.Count; i++)
+ {
+ _mobiles[i].Delete();
+ }
+
+ for (var i = 0; i < _items.Count; i++)
+ {
+ _items[i].Delete();
+ }
+ }
+}
+
+///
+/// A plain melee creature. Its speeds are pinned so tests do not depend on the npc-speeds table.
+///
+internal class TestCreature : BaseCreature
+{
+ public TestCreature() : base(AIType.AI_Melee) => Body = 0xC9;
+
+ public TestCreature(Serial serial) : base(serial) => Body = 0xC9;
+
+ public DateTime SummonEndValue => SummonEnd;
+
+ public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
+ {
+ activeSpeed = 0.3;
+ passiveSpeed = 0.6;
+ }
+
+ public override void GetMoveSpeeds(out double activeMoveSpeed, out double passiveMoveSpeed)
+ {
+ activeMoveSpeed = 0.6;
+ passiveMoveSpeed = 1.2;
+ }
+}
+
+///
+/// A vendor with no outfit and pinned speeds. Subclasses stock it by overriding
+/// , which runs from the base constructor.
+///
+internal class TestVendor : BaseVendor
+{
+ // Initialized before the base constructor calls InitSBInfo.
+ private readonly List _sbInfos = [];
+
+ public TestVendor() : base("the stub")
+ {
+ }
+
+ public TestVendor(Serial serial) : base(serial)
+ {
+ }
+
+ protected override List SBInfos => _sbInfos;
+
+ public override void InitSBInfo() => AddSBInfos(_sbInfos);
+
+ protected virtual void AddSBInfos(List sbInfos)
+ {
+ }
+
+ public override void InitOutfit()
+ {
+ }
+
+ public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
+ {
+ activeSpeed = 0.3;
+ passiveSpeed = 0.6;
+ }
+}
diff --git a/Projects/UOContent.Tests/Helpers/StepCacheStateScope.cs b/Projects/UOContent.Tests/Helpers/StepCacheStateScope.cs
new file mode 100644
index 000000000..39e22fb63
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/StepCacheStateScope.cs
@@ -0,0 +1,45 @@
+using System;
+using Server.Engines.Pathing.Cache;
+using Server.Systems.FeatureFlags;
+
+namespace Server.Tests;
+
+///
+/// Snapshots the pathfinding cache's tuning knobs and feature flag, and restores them on dispose
+/// along with empty caches. StepCache and MultiMaskCache are process-wide singletons and
+/// leaves the knobs alone, so a test that tunes one would otherwise
+/// leak it into every test that runs after it.
+///
+internal sealed class StepCacheStateScope : IDisposable
+{
+ private readonly int _missPromotionThreshold;
+ private readonly uint _missPromotionWindowMs;
+ private readonly int _maxResidentChunks;
+ private readonly bool _preloadOnLazyOpen;
+ private readonly bool _cacheEnabled;
+
+ public StepCacheStateScope()
+ {
+ var cache = StepCache.Instance;
+ _missPromotionThreshold = cache.MissPromotionThreshold;
+ _missPromotionWindowMs = cache.MissPromotionWindowMs;
+ _maxResidentChunks = cache.MaxResidentChunks;
+ _preloadOnLazyOpen = cache.PreloadOnLazyOpen;
+ _cacheEnabled = ContentFeatureFlags.BitmapPathfindingCache;
+ }
+
+ public void Dispose()
+ {
+ var cache = StepCache.Instance;
+
+ // Clear first: it closes any lazy reader a test opened under the knobs it set.
+ cache.Clear();
+ MultiMaskCache.Instance.Clear();
+
+ cache.MissPromotionThreshold = _missPromotionThreshold;
+ cache.MissPromotionWindowMs = _missPromotionWindowMs;
+ cache.MaxResidentChunks = _maxResidentChunks;
+ cache.PreloadOnLazyOpen = _preloadOnLazyOpen;
+ ContentFeatureFlags.BitmapPathfindingCache = _cacheEnabled;
+ }
+}
diff --git a/Projects/UOContent.Tests/Helpers/TempDirectory.cs b/Projects/UOContent.Tests/Helpers/TempDirectory.cs
new file mode 100644
index 000000000..a88f54d19
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/TempDirectory.cs
@@ -0,0 +1,44 @@
+using System;
+using System.IO;
+
+namespace Server.Tests;
+
+///
+/// A uniquely named directory under the system temp path, deleted with its contents on dispose so a
+/// failing assertion cannot leave files behind.
+///
+internal sealed class TempDirectory : IDisposable
+{
+ public TempDirectory(string prefix = "modernuo-test")
+ {
+ Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"{prefix}-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(Path);
+ }
+
+ public string Path { get; }
+
+ public string Combine(string relativePath) => System.IO.Path.Combine(Path, relativePath);
+
+ /// Writes to and returns its full path.
+ public string WriteFile(string relativePath, string content)
+ {
+ var path = Combine(relativePath);
+ File.WriteAllText(path, content);
+ return path;
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ if (Directory.Exists(Path))
+ {
+ Directory.Delete(Path, true);
+ }
+ }
+ catch (IOException)
+ {
+ // A handle still open on Windows; the OS temp cleaner reclaims it.
+ }
+ }
+}
diff --git a/Projects/UOContent.Tests/Helpers/TimerWheelScope.cs b/Projects/UOContent.Tests/Helpers/TimerWheelScope.cs
new file mode 100644
index 000000000..daabd3188
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/TimerWheelScope.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Reflection;
+
+namespace Server.Tests;
+
+///
+/// Gives a test its own simulated clock and an empty timer wheel, and puts the process-wide ones
+/// back on dispose. Core's tick, Core.Now and the wheel are statics shared by every test in the
+/// host, so a test that resets them without restoring hands its clock to whatever runs next.
+///
+///
+/// Timers started inside the scope are stopped on dispose; timers that were scheduled before it
+/// resume on the restored wheel.
+///
+internal sealed class TimerWheelScope : IDisposable
+{
+ private const BindingFlags PrivateStatic = BindingFlags.NonPublic | BindingFlags.Static;
+
+ private static readonly FieldInfo _ringsField = GetTimerField("_rings");
+ private static readonly FieldInfo _ringIndexesField = GetTimerField("_ringIndexes");
+ private static readonly FieldInfo _lastTickTurnedField = GetTimerField("_lastTickTurned");
+
+ private readonly long _savedTick;
+ private readonly DateTime _savedNow;
+ private readonly Timer[][] _savedRings;
+ private readonly int[] _savedRingIndexes;
+ private readonly long _savedLastTickTurned;
+ private bool _disposed;
+
+ public TimerWheelScope(long startTick = 0)
+ {
+ _savedTick = Core._tickCount;
+ _savedNow = Core._now;
+ _savedRings = (Timer[][])Rings.Clone();
+ _savedRingIndexes = (int[])RingIndexes.Clone();
+ _savedLastTickTurned = (long)_lastTickTurnedField.GetValue(null)!;
+
+ Core._tickCount = startTick;
+ Timer.Init(startTick);
+ }
+
+ private static Timer[][] Rings => (Timer[][])_ringsField.GetValue(null)!;
+
+ private static int[] RingIndexes => (int[])_ringIndexesField.GetValue(null)!;
+
+ /// The wheel's tick rate: one turn per call keeps it in lockstep with Core.TickCount.
+ public static int Step => Timer.TickRate;
+
+ /// Advances the clock by , turning the wheel every step.
+ public static void RunFor(long ms)
+ {
+ var deadline = Core._tickCount + ms;
+
+ while (Core._tickCount - deadline < 0)
+ {
+ Core._tickCount += Step;
+ Timer.Slice(Core._tickCount);
+ }
+ }
+
+ ///
+ /// Advances the clock step by step until holds or
+ /// has passed. Returns the condition's final value.
+ ///
+ public static bool RunUntil(Func condition, long maxMs)
+ {
+ var deadline = Core._tickCount + maxMs;
+
+ while (Core._tickCount - deadline < 0)
+ {
+ if (condition())
+ {
+ return true;
+ }
+
+ Core._tickCount += Step;
+ Timer.Slice(Core._tickCount);
+ }
+
+ return condition();
+ }
+
+ /// Jumps the tick and Core.Now forward together, then fires everything that came due.
+ public static void Advance(TimeSpan time)
+ {
+ Core._now += time;
+ Core._tickCount += (long)time.TotalMilliseconds;
+ Timer.Slice(Core._tickCount);
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ StopAll(Rings);
+
+ var rings = Rings;
+ for (var i = 0; i < rings.Length; i++)
+ {
+ rings[i] = _savedRings[i];
+ }
+
+ Array.Copy(_savedRingIndexes, RingIndexes, _savedRingIndexes.Length);
+ _lastTickTurnedField.SetValue(null, _savedLastTickTurned);
+
+ Core._tickCount = _savedTick;
+ Core._now = _savedNow;
+ }
+
+ // Stop() unlinks the slot's head and promotes the next timer, so draining heads empties a slot.
+ private static void StopAll(Timer[][] rings)
+ {
+ for (var i = 0; i < rings.Length; i++)
+ {
+ var ring = rings[i];
+ for (var slot = 0; slot < ring.Length; slot++)
+ {
+ Timer head;
+ while ((head = ring[slot]) != null)
+ {
+ head.Stop();
+
+ if (ring[slot] == head)
+ {
+ // A head that was already stopped never unlinks itself.
+ ring[slot] = null;
+ }
+ }
+ }
+ }
+ }
+
+ private static FieldInfo GetTimerField(string name) =>
+ typeof(Timer).GetField(name, PrivateStatic)
+ ?? throw new InvalidOperationException($"Timer.{name} not found via reflection; the timer wheel was refactored.");
+}
diff --git a/Projects/UOContent.Tests/Tests/Accounting/AccountPasswordTests.cs b/Projects/UOContent.Tests/Tests/Accounting/AccountPasswordTests.cs
index 29a1b818f..c0901ca48 100644
--- a/Projects/UOContent.Tests/Tests/Accounting/AccountPasswordTests.cs
+++ b/Projects/UOContent.Tests/Tests/Accounting/AccountPasswordTests.cs
@@ -1,6 +1,7 @@
using System;
using Server.Accounting;
using Server.Accounting.Security;
+using Server.Tests.Accounting.Security;
using Xunit;
namespace Server.Tests.Accounting;
@@ -64,7 +65,9 @@ public class AccountPasswordTests : IDisposable
"$argon2i$v=19$m=8192,t=3,p=1$LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw";
Assert.True(account.CheckPassword(Password));
- Assert.StartsWith("$argon2id$v=19$m=16384,t=1,p=1$", account.Password);
+ // The cost parameters are shard-tunable; the variant and version are not.
+ Assert.StartsWith("$argon2id$v=19$", account.Password);
+ Assert.StartsWith(Argon2TestParameters.CurrentPrefix, account.Password);
// Already current: verifying again must not rewrite the hash.
var afterFirst = account.Password;
diff --git a/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs b/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs
index df499509e..3412feab9 100644
--- a/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs
+++ b/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs
@@ -1,7 +1,9 @@
using System;
+using System.Collections.Generic;
using System.Threading;
using Server.Accounting;
using Server.Accounting.Security;
+using Server.Tests.Accounting.Security;
using Xunit;
namespace Server.Tests.Accounting;
@@ -13,12 +15,27 @@ public class PasswordWorkerTests : IDisposable
private readonly PasswordProtectionAlgorithm _originalAlgorithm = AccountSecurity.CurrentAlgorithm;
+ // The constructor registers each account in the process-wide Accounts table.
+ private readonly List _created = [];
+
public PasswordWorkerTests() => AccountSecurity.CurrentAlgorithm = PasswordProtectionAlgorithm.Argon2;
- public void Dispose() => AccountSecurity.CurrentAlgorithm = _originalAlgorithm;
+ public void Dispose()
+ {
+ for (var i = 0; i < _created.Count; i++)
+ {
+ _created[i].Delete();
+ }
- private static Account CreateAccount(string username) =>
- Accounts.GetAccount(username) as Account ?? new Account(username, Password);
+ AccountSecurity.CurrentAlgorithm = _originalAlgorithm;
+ }
+
+ private Account CreateAccount(string username)
+ {
+ var account = new Account(username, Password);
+ _created.Add(account);
+ return account;
+ }
///
/// Enqueues work, then pumps the loop context until or the deadline.
@@ -136,7 +153,8 @@ public class PasswordWorkerTests : IDisposable
var outcome = PasswordWorker.ComputeInline(JobFor(account, Password));
Assert.True(outcome.Verified);
- Assert.StartsWith("$argon2id$v=19$m=16384,t=1,p=1$", outcome.Hash);
+ Assert.StartsWith("$argon2id$v=19$", outcome.Hash);
+ Assert.StartsWith(Argon2TestParameters.CurrentPrefix, outcome.Hash);
}
[Fact]
diff --git a/Projects/UOContent.Tests/Tests/Accounting/Security/Argon2TestParameters.cs b/Projects/UOContent.Tests/Tests/Accounting/Security/Argon2TestParameters.cs
new file mode 100644
index 000000000..8e62573ac
--- /dev/null
+++ b/Projects/UOContent.Tests/Tests/Accounting/Security/Argon2TestParameters.cs
@@ -0,0 +1,55 @@
+using System;
+using Server.Accounting.Security;
+
+namespace Server.Tests.Accounting.Security;
+
+///
+/// The PHC parameter prefix the configured writes, read from
+/// a real hash so a shard that tunes the Argon2 cost does not break assertions about the format.
+///
+internal static class Argon2TestParameters
+{
+ /// For example $argon2id$v=19$m=16384,t=1,p=1$, ending at the salt.
+ public static readonly string CurrentPrefix = ReadPrefix();
+
+ public static string Type => CurrentPrefix.Split('$')[1];
+
+ public static string Version => CurrentPrefix.Split('$')[2];
+
+ public static int Memory => ReadCost('m');
+
+ public static int Time => ReadCost('t');
+
+ public static int Parallelism => ReadCost('p');
+
+ public static string BuildPrefix(string type, int memory, int time, int parallelism) =>
+ $"${type}${Version}$m={memory},t={time},p={parallelism}$";
+
+ private static string ReadPrefix()
+ {
+ var hash = Argon2PasswordProtection.Instance.EncryptPassword("prefix-probe");
+
+ // $type$version$params$salt$digest: the prefix runs through the fourth '$'.
+ var end = 0;
+ for (var i = 0; i < 4; i++)
+ {
+ end = hash.IndexOf('$', end) + 1;
+ }
+
+ return hash[..end];
+ }
+
+ private static int ReadCost(char key)
+ {
+ var costs = CurrentPrefix.Split('$')[3].Split(',');
+ for (var i = 0; i < costs.Length; i++)
+ {
+ if (costs[i][0] == key)
+ {
+ return int.Parse(costs[i].AsSpan(2));
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs b/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs
index dc16a7f5b..382a4bc0d 100644
--- a/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs
+++ b/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs
@@ -113,34 +113,60 @@ public class PasswordProtectionTest
Assert.False(Argon2PasswordProtection.Instance.ValidatePassword(LegacyArgon2iHash, "wrong"));
}
- [Theory]
- // type, memory, time, parallelism -> expected NeedsRehash
- [InlineData("argon2id", 16384, 1, 1, false)] // current defaults
- [InlineData("argon2i", 8192, 3, 1, true)] // the old shipping default
- [InlineData("argon2id", 8192, 1, 1, true)] // right type, stale memory
- [InlineData("argon2id", 16384, 3, 1, true)] // right type, stale iterations
- [InlineData("argon2id", 16384, 1, 2, true)] // right type, stale parallelism
- [InlineData("argon2i", 16384, 1, 1, true)] // right cost, stale type
- public void Argon2_NeedsRehash_ComparesTypeAndCost(
- string type, int memory, int time, int parallelism, bool expected
- )
- {
- var hash = $"${type}$v=19$m={memory},t={time},p={parallelism}$" +
- "LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw";
+ private const string SaltAndDigest = "LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw";
- Assert.Equal(expected, Argon2PasswordProtection.Instance.NeedsRehash(hash));
+ [Fact]
+ public void Argon2_NeedsRehash_IsFalseForCurrentParameters()
+ {
+ var hash = Argon2TestParameters.CurrentPrefix + SaltAndDigest;
+
+ Assert.False(Argon2PasswordProtection.Instance.NeedsRehash(hash));
+ }
+
+ // Each case differs from the configured parameters in exactly one field, so the test holds
+ // whatever cost a shard configures.
+ [Theory]
+ [InlineData("type")]
+ [InlineData("memory")]
+ [InlineData("time")]
+ [InlineData("parallelism")]
+ public void Argon2_NeedsRehash_ComparesTypeAndCost(string staleField)
+ {
+ var type = Argon2TestParameters.Type;
+ var memory = Argon2TestParameters.Memory;
+ var time = Argon2TestParameters.Time;
+ var parallelism = Argon2TestParameters.Parallelism;
+ var otherType = type == "argon2id" ? "argon2i" : "argon2id";
+
+ (type, memory, time, parallelism) = staleField switch
+ {
+ "type" => (otherType, memory, time, parallelism),
+ "memory" => (type, memory * 2, time, parallelism),
+ "time" => (type, memory, time + 1, parallelism),
+ _ => (type, memory, time, parallelism + 1)
+ };
+
+ var hash = Argon2TestParameters.BuildPrefix(type, memory, time, parallelism) + SaltAndDigest;
+
+ Assert.True(Argon2PasswordProtection.Instance.NeedsRehash(hash));
+ }
+
+ [Fact]
+ public void Argon2_NeedsRehash_IsTrueForLegacyShippingDefault()
+ {
+ Assert.True(Argon2PasswordProtection.Instance.NeedsRehash(LegacyArgon2iHash));
}
// Digest and salt lengths are decoded base64 sizes rather than parameter-list entries, so they
// need their own literals. Current type and cost throughout; only a length differs.
[Theory]
// 16-byte digest: 22 base64 chars instead of the 43 a 32-byte digest encodes to.
- [InlineData("$argon2id$v=19$m=16384,t=1,p=1$LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4g")]
+ [InlineData("LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4g")]
// 8-byte salt: 11 base64 chars instead of the 22 a 16-byte salt encodes to.
- [InlineData("$argon2id$v=19$m=16384,t=1,p=1$LD1XJz7P3wQ$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw")]
- public void Argon2_NeedsRehash_ComparesSaltAndDigestLengths(string hash)
+ [InlineData("LD1XJz7P3wQ$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw")]
+ public void Argon2_NeedsRehash_ComparesSaltAndDigestLengths(string saltAndDigest)
{
- Assert.True(Argon2PasswordProtection.Instance.NeedsRehash(hash));
+ Assert.True(Argon2PasswordProtection.Instance.NeedsRehash(Argon2TestParameters.CurrentPrefix + saltAndDigest));
}
[Theory]
diff --git a/Projects/UOContent.Tests/Tests/Commands/ChainedBindingSortTests.cs b/Projects/UOContent.Tests/Tests/Commands/ChainedBindingSortTests.cs
index 80e664647..dbeba969e 100644
--- a/Projects/UOContent.Tests/Tests/Commands/ChainedBindingSortTests.cs
+++ b/Projects/UOContent.Tests/Tests/Commands/ChainedBindingSortTests.cs
@@ -4,6 +4,7 @@ using Server;
using Server.Commands;
using Server.Commands.Generic;
using Server.Items;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Commands;
@@ -14,31 +15,13 @@ namespace UOContent.Tests.Commands;
[Collection("Sequential UOContent Tests")]
public class ChainedBindingSortTests : IDisposable
{
- private readonly List
- _items = [];
+ private readonly CommandTestHarness _harness = new();
- public void Dispose()
- {
- for (var i = 0; i < _items.Count; i++)
- {
- _items[i].Delete();
- }
+ public void Dispose() => _harness.Dispose();
- _items.Clear();
- }
+ private SkillTeleporter Teleporter(TextDefinition message) => _harness.Teleporter(message);
- private SkillTeleporter Teleporter(TextDefinition message)
- {
- var tp = new SkillTeleporter { Message = message };
- _items.Add(tp);
- return tp;
- }
-
- private static Property Bind(string binding)
- {
- var prop = new Property(binding);
- prop.BindTo(typeof(SkillTeleporter), PropertyAccess.Read);
- return prop;
- }
+ private static Property Bind(string binding) => CommandTestHarness.Bind(typeof(SkillTeleporter), binding);
private static IComparer
[Collection("Sequential Pathfinding Tests")]
-public class BitmapAStarAlgorithmTests
+public class BitmapAStarAlgorithmTests : IDisposable
{
private readonly ITestOutputHelper _output;
+ private readonly StepCacheStateScope _cacheState = new();
public BitmapAStarAlgorithmTests(ITestOutputHelper output)
{
_output = output;
}
+ public void Dispose() => _cacheState.Dispose();
+
[Theory]
// Pinned cell (1500, 1600, z=10): mask=0xC1 → N, W, NW walkable.
// Use start.Z for goal.Z so destNode lands in the same Z plane the algorithm reaches
@@ -33,22 +37,27 @@ public class BitmapAStarAlgorithmTests
public void DefaultWalker_FindsPath_ViaCacheFastPath(int sx, int sy, int gx, int gy)
{
StepCache.Instance.Clear();
+ StepCache.Instance.MissPromotionThreshold = 1;
var map = Map.Maps[1];
Assert.NotNull(map);
- var stub = new DefaultWalkerStub();
+ var stub = new WalkerStub();
map.GetAverageZ(sx, sy, out _, out var startZ, out _);
var start = new Point3D(sx, sy, (sbyte)startZ);
var goal = new Point3D(gx, gy, (sbyte)startZ);
stub.MoveToWorld(start, map);
+ var statsBefore = StepCache.Instance.GetStats();
var result = BitmapAStarAlgorithm.Instance.Find(stub, map, start, goal);
+ var statsAfter = StepCache.Instance.GetStats();
stub.Delete();
Assert.NotNull(result);
Assert.NotEmpty(result);
+ Assert.True(statsAfter.BuildsTotal > statsBefore.BuildsTotal,
+ "a default walker should expand through the cache, not the slow path");
_output.WriteLine($"default-walker ({sx},{sy})->({gx},{gy}): {result.Length} steps");
}
@@ -62,7 +71,7 @@ public class BitmapAStarAlgorithmTests
var map = Map.Maps[1];
Assert.NotNull(map);
- var stub = new SwimmingStub(World.NewMobile);
+ var stub = new BareCreatureStub(World.NewMobile);
stub.DefaultMobileInit();
stub.CanSwim = true; // overlay route — cache + (walkMask | wetMask&canSwim)
map.GetAverageZ(sx, sy, out _, out var startZ, out _);
@@ -104,13 +113,13 @@ public class BitmapAStarAlgorithmTests
var start = new Point3D(sx, sy, (sbyte)startZ);
var goal = new Point3D(gx, gy, (sbyte)startZ);
- var walker = new DefaultWalkerStub();
+ var walker = new WalkerStub();
walker.MoveToWorld(start, map);
// Living blocker on the only direct-line cell. CanMoveOver returns false for an
// alive non-staff mobile, so the dynamic-obstacle pass must reject this cell.
map.GetAverageZ(blockX, blockY, out _, out var blockZ, out _);
- var blocker = new DefaultWalkerStub();
+ var blocker = new WalkerStub();
blocker.MoveToWorld(new Point3D(blockX, blockY, (sbyte)blockZ), map);
var result = BitmapAStarAlgorithm.Instance.Find(walker, map, start, goal);
@@ -146,24 +155,14 @@ public class BitmapAStarAlgorithmTests
var blockX = 1499;
var blockY = 1599;
- // Find any ItemID whose TileData has ImpassableSurface so the dynamic pass
- // rejects the cell. Pinning to a specific ID would couple the test to UO art data.
- ushort blockerItemId = 0;
- for (ushort id = 1; id < TileData.MaxItemValue; id++)
- {
- if (TileData.ItemTable[id].ImpassableSurface)
- {
- blockerItemId = id;
- break;
- }
- }
+ var blockerItemId = TestTiles.FirstImpassableItemId();
Assert.NotEqual(0, blockerItemId);
map.GetAverageZ(sx, sy, out _, out var startZ, out _);
var start = new Point3D(sx, sy, (sbyte)startZ);
var goal = new Point3D(gx, gy, (sbyte)startZ);
- var walker = new DefaultWalkerStub();
+ var walker = new WalkerStub();
walker.MoveToWorld(start, map);
map.GetAverageZ(blockX, blockY, out _, out var blockZ, out _);
@@ -197,28 +196,20 @@ public class BitmapAStarAlgorithmTests
var map = Map.Maps[1];
Assert.NotNull(map);
- var stub = new DefaultWalkerStub();
+ var stub = new WalkerStub();
map.GetAverageZ(1500, 1600, out _, out var startZ, out _);
var start = new Point3D(1500, 1600, (sbyte)startZ);
var goal = new Point3D(1498, 1598, (sbyte)startZ);
stub.MoveToWorld(start, map);
var statsBefore = StepCache.Instance.GetStats();
- var prevFlag = ContentFeatureFlags.BitmapPathfindingCache;
- try
- {
- ContentFeatureFlags.BitmapPathfindingCache = false;
- var result = BitmapAStarAlgorithm.Instance.Find(stub, map, start, goal);
- Assert.NotNull(result);
- Assert.NotEmpty(result);
- }
- finally
- {
- ContentFeatureFlags.BitmapPathfindingCache = prevFlag;
- }
+ ContentFeatureFlags.BitmapPathfindingCache = false;
+ var result = BitmapAStarAlgorithm.Instance.Find(stub, map, start, goal);
var statsAfter = StepCache.Instance.GetStats();
stub.Delete();
+ Assert.NotNull(result);
+ Assert.NotEmpty(result);
Assert.Equal(statsBefore.BuildsTotal, statsAfter.BuildsTotal);
}
@@ -346,7 +337,7 @@ public class BitmapAStarAlgorithmTests
StepCache.Instance.MissPromotionThreshold = 2;
var map = Map.Maps[1];
- var stub = new DefaultWalkerStub();
+ var stub = new WalkerStub();
map.GetAverageZ(1500, 1600, out _, out var startZ, out _);
var start = new Point3D(1500, 1600, (sbyte)startZ);
var goal = new Point3D(1498, 1598, (sbyte)startZ);
@@ -376,7 +367,7 @@ public class BitmapAStarAlgorithmTests
StepCache.Instance.MissPromotionWindowMs = 30_000;
var map = Map.Maps[1];
- var stub = new DefaultWalkerStub();
+ var stub = new WalkerStub();
map.GetAverageZ(1500, 1600, out _, out var startZ, out _);
var start = new Point3D(1500, 1600, (sbyte)startZ);
var goal = new Point3D(1498, 1598, (sbyte)startZ);
@@ -401,18 +392,6 @@ public class BitmapAStarAlgorithmTests
);
}
- ///
- /// Plain Mobile — RequiresSlowPath returns false, the bitmap algorithm uses the cache
- /// fast path on every expansion.
- ///
- private sealed class DefaultWalkerStub : Mobile
- {
- public DefaultWalkerStub()
- {
- Body = 0xC9;
- }
- }
-
///
/// Mobile with Player=true and default AccessLevel (Player). Triggers the strict
/// AND-rule for diagonal corner-cut while still using the cache.
@@ -426,20 +405,6 @@ public class BitmapAStarAlgorithmTests
}
}
- ///
- /// BaseCreature with CanSwim=true — uses the cache via capability overlay (walk OR
- /// (wet AND canSwim)). Use the Serial constructor (deserialization path) to bypass
- /// NPCSpeeds init, which requires the npc-speeds.json table loaded — not available
- /// in tests.
- ///
- private sealed class SwimmingStub : BaseCreature
- {
- public SwimmingStub(Serial serial) : base(serial)
- {
- Body = 0xC9;
- }
- }
-
///
/// BaseCreature with CanFly=true — RequiresSlowPath returns true (Z-jumping is beyond
/// the cache's static-only scope), so GetSuccessors short-circuits to the slow path.
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs
index 8ecbd0591..5ecb0cdc1 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs
@@ -1,3 +1,4 @@
+using System;
using Server.Engines.Pathing.Cache;
using Server.Items;
using Server.Mobiles;
@@ -9,25 +10,22 @@ using CalcMoves = Server.Movement.Movement;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class HousePathRoutingTests
+public class HousePathRoutingTests : IDisposable
{
private readonly ITestOutputHelper _output;
+ private readonly StepCacheStateScope _cacheState = new();
public HousePathRoutingTests(ITestOutputHelper output) => _output = output;
- private const int MapId = 1;
+ public void Dispose() => _cacheState.Dispose();
- private sealed class WalkerStub : Mobile
- {
- public WalkerStub() => Body = 0xC9;
- }
+ private const int MapId = 1;
[SkippableFact]
public void PathAround_NeverTraversesAWallCell()
{
TileDataRequirement.SkipIfMissing();
StepCache.Instance.Clear();
- var prevThreshold = StepCache.Instance.MissPromotionThreshold;
StepCache.Instance.MissPromotionThreshold = 1;
var map = Map.Maps[MapId];
Assert.NotNull(map);
@@ -69,7 +67,6 @@ public class HousePathRoutingTests
}
finally
{
- StepCache.Instance.MissPromotionThreshold = prevThreshold;
walker.Delete();
multi.Delete();
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiCacheUsedTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiCacheUsedTests.cs
index fc9e58898..50a27ee44 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiCacheUsedTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiCacheUsedTests.cs
@@ -1,3 +1,4 @@
+using System;
using Server.Engines.Pathing.Cache;
using Server.PathAlgorithms;
using Server.Systems.FeatureFlags;
@@ -6,20 +7,25 @@ using Xunit;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class MultiCacheUsedTests
+public class MultiCacheUsedTests : IDisposable
{
private const int MapId = 1; // Trammel
private const int GuildHouseId = 0x74;
- [Fact]
+ private readonly StepCacheStateScope _cacheState = new();
+
+ public void Dispose() => _cacheState.Dispose();
+
+ [SkippableFact]
public void PathNearMulti_IncrementsMultiLocalHits()
{
+ // Without the client's multi data the house has no footprint for the path to skirt.
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[MapId];
map.GetAverageZ(1480, 1620, out _, out var z, out _);
var houseLoc = new Point3D(1480, 1620, (sbyte)z);
var multi = new TestMulti(GuildHouseId);
- var cacheWas = ContentFeatureFlags.BitmapPathfindingCache;
ContentFeatureFlags.BitmapPathfindingCache = true;
var mover = MultiTestSupport.GetWalkerOracle(map, new Point3D(1480, 1630, (sbyte)z));
@@ -44,7 +50,6 @@ public class MultiCacheUsedTests
{
mover.Delete();
multi.Delete();
- ContentFeatureFlags.BitmapPathfindingCache = cacheWas;
}
}
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs
index 38a6824ea..2b2adf93c 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs
@@ -1,3 +1,4 @@
+using System;
using Server.Engines.Pathing.Cache;
using Server.Items;
using Xunit;
@@ -5,13 +6,17 @@ using Xunit;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class MultiMaskCacheTests
+public class MultiMaskCacheTests : IDisposable
{
private const int MapId = 1;
private const int GuildHouseId = 0x74;
private const int PlaceX = 1480;
private const int PlaceY = 1620;
+ private readonly StepCacheStateScope _cacheState = new();
+
+ public void Dispose() => _cacheState.Dispose();
+
[SkippableFact]
public void TryResolveCoveringMulti_FindsPlacedMulti_AndLocalIndices()
{
@@ -99,7 +104,6 @@ public class MultiMaskCacheTests
map.GetAverageZ(CleanX, CleanY, out _, out var z, out _);
var houseLoc = new Point3D(CleanX, CleanY, (sbyte)z);
var multi = new TestMulti(GuildHouseId);
- var cacheWas = Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache;
Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache = true;
var mover = MultiTestSupport.GetWalkerOracle(map, new Point3D(CleanX, CleanY + 10, (sbyte)z));
try
@@ -124,17 +128,17 @@ public class MultiMaskCacheTests
{
mover.Delete();
multi.Delete();
- Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache = cacheWas;
}
}
- [Fact]
+ [SkippableFact]
public void ClutteredHouse_DoesNotServeCache_ButStillPaths()
{
+ // The clutter is Britain's statics; on a blank map the house has no footprint to dirty.
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[MapId];
map.GetAverageZ(PlaceX, PlaceY, out _, out var z, out _);
var multi = new TestMulti(GuildHouseId);
- var cacheWas = Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache;
Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache = true;
var mover = MultiTestSupport.GetWalkerOracle(map, new Point3D(PlaceX, PlaceY + 10, (sbyte)z));
try
@@ -159,7 +163,6 @@ public class MultiMaskCacheTests
{
mover.Delete();
multi.Delete();
- Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache = cacheWas;
}
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiPathInvariantTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiPathInvariantTests.cs
index 9a920282f..9ec598916 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiPathInvariantTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiPathInvariantTests.cs
@@ -1,3 +1,4 @@
+using System;
using Server.Engines.Pathing.Cache;
using Server.Items;
using Server.Mobiles;
@@ -9,41 +10,33 @@ using Xunit.Abstractions;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class MultiPathInvariantTests
+public class MultiPathInvariantTests : IDisposable
{
private readonly ITestOutputHelper _output;
+ private readonly StepCacheStateScope _cacheState = new();
public MultiPathInvariantTests(ITestOutputHelper output) => _output = output;
- private const int MapId = 1;
+ public void Dispose() => _cacheState.Dispose();
- private sealed class WalkerStub : Mobile
- {
- public WalkerStub() => Body = 0xC9;
- }
+ private const int MapId = 1;
private static Direction[] FindWithFlag(Mobile m, Map map, Point3D start, Point3D goal, bool cacheOn)
{
- var prev = ContentFeatureFlags.BitmapPathfindingCache;
- try
- {
- ContentFeatureFlags.BitmapPathfindingCache = cacheOn;
- StepCache.Instance.Clear();
- StepCache.Instance.MissPromotionThreshold = 1;
- return BitmapAStarAlgorithm.Instance.Find(m, map, start, goal);
- }
- finally
- {
- ContentFeatureFlags.BitmapPathfindingCache = prev;
- }
+ ContentFeatureFlags.BitmapPathfindingCache = cacheOn;
+ StepCache.Instance.Clear();
+ StepCache.Instance.MissPromotionThreshold = 1;
+ return BitmapAStarAlgorithm.Instance.Find(m, map, start, goal);
}
- [Theory]
+ [SkippableTheory]
// start, goal: straddle a house placed between them (house at ~ midpoint).
[InlineData(1500, 1600, 1500, 1612)] // N-S across the footprint
[InlineData(1494, 1606, 1512, 1606)] // E-W across the footprint
public void Find_CacheOn_EqualsCacheOff_WithHousePresent(int sx, int sy, int gx, int gy)
{
+ // Without the client's multi data the house has no footprint and nothing stands in the way.
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[MapId];
Assert.NotNull(map);
@@ -100,11 +93,11 @@ public class MultiPathInvariantTests
{
var on = FindWithFlag(walker, map, start, goal, cacheOn: true);
var off = FindWithFlag(walker, map, start, goal, cacheOn: false);
- Assert.Equal(off == null, on == null);
- if (on != null)
- {
- Assert.Equal(off, on);
- }
+
+ // Two nulls would agree about nothing: the control route must exist.
+ Assert.NotNull(off);
+ Assert.NotNull(on);
+ Assert.Equal(off, on);
}
finally
{
@@ -112,9 +105,10 @@ public class MultiPathInvariantTests
}
}
- [Fact]
+ [SkippableFact]
public void Find_CacheOn_EqualsCacheOff_RoutesAroundHouse()
{
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[MapId];
Assert.NotNull(map);
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs
index 1103cdb83..8f20d04f4 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs
@@ -1,3 +1,4 @@
+using System;
using Server.Engines.Pathing.Cache;
using Server.Items;
using Xunit;
@@ -6,12 +7,15 @@ using Xunit.Abstractions;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class MultiSplitRoutingTests
+public class MultiSplitRoutingTests : IDisposable
{
private readonly ITestOutputHelper _output;
+ private readonly StepCacheStateScope _cacheState = new();
public MultiSplitRoutingTests(ITestOutputHelper output) => _output = output;
+ public void Dispose() => _cacheState.Dispose();
+
// Open plain on Trammel used by existing pathfinding tests; room for a 7x7 house.
private const int MapId = 1;
private const int PlaceX = 1500;
@@ -60,7 +64,6 @@ public class MultiSplitRoutingTests
public void CleanMap_AfterMultiRemoved_ServesStaticHitAgain()
{
StepCache.Instance.Clear();
- var prevThreshold = StepCache.Instance.MissPromotionThreshold;
StepCache.Instance.MissPromotionThreshold = 1; // build on first touch
var map = Map.Maps[MapId];
Assert.NotNull(map);
@@ -72,20 +75,13 @@ public class MultiSplitRoutingTests
var sector = map.GetRealSector(PlaceX >> 4, PlaceY >> 4);
Assert.True(sector.HasMultis, "placing a multi must set Sector.HasMultis");
- try
- {
- multi.Delete();
- Assert.False(sector.HasMultis, "removing the only multi must clear HasMultis");
+ multi.Delete();
+ Assert.False(sector.HasMultis, "removing the only multi must clear HasMultis");
- // A cell that is NOT a static-fallthrough kind (e.g. off-map / multi) should now be
- // eligible for a real static answer. Use the placement center, which is open plain.
- var mask = StepCache.Instance.TryGetMask(map, PlaceX, PlaceY, (sbyte)z);
- Assert.True(mask.IsHit, $"expected a real static answer after removal, got {mask.HitKind}");
- _output.WriteLine($"post-removal hitKind at center: {mask.HitKind}");
- }
- finally
- {
- StepCache.Instance.MissPromotionThreshold = prevThreshold;
- }
+ // A cell that is NOT a static-fallthrough kind (e.g. off-map / multi) should now be
+ // eligible for a real static answer. Use the placement center, which is open plain.
+ var mask = StepCache.Instance.TryGetMask(map, PlaceX, PlaceY, (sbyte)z);
+ Assert.True(mask.IsHit, $"expected a real static answer after removal, got {mask.HitKind}");
+ _output.WriteLine($"post-removal hitKind at center: {mask.HitKind}");
}
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiTestSupport.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiTestSupport.cs
index 092b28711..0f06a44aa 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiTestSupport.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiTestSupport.cs
@@ -20,12 +20,6 @@ public sealed class TestMulti : BaseMulti
}
}
-/// Default walker body, shared across pathfinding test fixtures.
-public sealed class WalkerStub : Mobile
-{
- public WalkerStub() => Body = 0xC9;
-}
-
///
/// Stand-in for a customizable foundation: Components is a swappable MCL, exactly the
/// runtime-mutation shape HouseFoundation uses (it replaces its MCL wholesale on redesign
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs
index d8d46e7f8..1f993d477 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs
@@ -18,11 +18,6 @@ public class MultiWalkabilityTests
private const int PlaceX = 1500;
private const int PlaceY = 1600;
- private sealed class WalkerStub : Mobile
- {
- public WalkerStub() => Body = 0xC9;
- }
-
[SkippableFact]
public void WallCell_CannotBeEnteredFromAnyAdjacentCell()
{
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs
index 0930bad7a..efb434738 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs
@@ -19,7 +19,7 @@ public class PathfindRecorderTests
try
{
- var stub = new RecorderStub(World.NewMobile);
+ var stub = new BareCreatureStub(World.NewMobile);
stub.DefaultMobileInit();
stub.MoveToWorld(new Point3D(1500, 1600, 0), Map.Maps[1]);
@@ -48,7 +48,7 @@ public class PathfindRecorderTests
{
Assert.True(PathfindRecorder.Enabled);
- var stub = new RecorderStub(World.NewMobile);
+ var stub = new BareCreatureStub(World.NewMobile);
stub.DefaultMobileInit();
stub.MoveToWorld(new Point3D(1500, 1600, 0), Map.Maps[1]);
@@ -89,7 +89,7 @@ public class PathfindRecorderTests
try
{
- var stub = new RecorderStub(World.NewMobile);
+ var stub = new BareCreatureStub(World.NewMobile);
stub.DefaultMobileInit();
stub.MoveToWorld(new Point3D(1500, 1600, 0), Map.Maps[1]);
stub.CanSwim = true;
@@ -100,13 +100,17 @@ public class PathfindRecorderTests
);
PathfindRecorder.SetEnabled(false);
+
+ // BaseCreature's own defaults for these two; the recording must carry whatever they are.
+ var canOpenDoors = JsonBool(stub.CanOpenDoors);
+ var canMoveOverObstacles = JsonBool(stub.CanMoveOverObstacles);
stub.Delete();
var content = File.ReadAllText(path);
Assert.Contains("\"CanSwim\":true", content);
Assert.Contains("\"CanFly\":false", content);
- Assert.Contains("\"CanOpenDoors\":", content); // value depends on BaseCreature defaults
- Assert.Contains("\"CanMoveOverObstacles\":", content); // — same.
+ Assert.Contains($"\"CanOpenDoors\":{canOpenDoors}", content);
+ Assert.Contains($"\"CanMoveOverObstacles\":{canMoveOverObstacles}", content);
}
finally
{
@@ -142,8 +146,5 @@ public class PathfindRecorderTests
}
}
- private sealed class RecorderStub : Server.Mobiles.BaseCreature
- {
- public RecorderStub(Serial serial) : base(serial) => Body = 0xC9;
- }
+ private static string JsonBool(bool value) => value ? "true" : "false";
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/PathingTestSupport.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/PathingTestSupport.cs
index 9027332c7..de0fdaaae 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/PathingTestSupport.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/PathingTestSupport.cs
@@ -4,8 +4,8 @@ using Server.Engines.Pathing.Cache;
namespace Server.Tests.Pathfinding;
///
-/// Shared fixtures for the step-cache tests: the walker the parity tests measure against, the
-/// cell-index arithmetic, and builders for the chunk state several tests inject by hand.
+/// Shared fixtures for the step-cache tests: the test map, the cell-index arithmetic, and builders
+/// for the chunk state several tests inject by hand.
///
internal static class PathingTestSupport
{
@@ -53,17 +53,4 @@ internal static class PathingTestSupport
return data;
}
-
- ///
- /// The default static walker. Deriving straight from rather than
- /// BaseCreature is the point: MovementImpl then sees no creature capabilities (no swim, no fly,
- /// no door-opening), which is exactly the walker the cache bakes for.
- ///
- public sealed class StaticWalker : Mobile
- {
- public StaticWalker()
- {
- Body = 0xC9;
- }
- }
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs
index d29b79ef9..4094c093f 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs
@@ -6,8 +6,12 @@ using Xunit;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class StepCacheFileTests
+public class StepCacheFileTests : IDisposable
{
+ private readonly StepCacheStateScope _cacheState = new();
+
+ public void Dispose() => _cacheState.Dispose();
+
///
/// Round-trip a populated cache through a .swb file under lazy loading: build chunks,
/// save, clear (closes lazy readers + drops residents), open as lazy backing store,
@@ -363,23 +367,16 @@ public class StepCacheFileTests
cache.Clear();
cache.PreloadOnLazyOpen = true;
- try
- {
- Assert.True(cache.TryOpenLazyReader(path, map.MapID));
+ Assert.True(cache.TryOpenLazyReader(path, map.MapID));
- // Every chunk should be resident — no further queries needed.
- Assert.Equal(coords.Length, cache.GetStats().ResidentChunks);
- Assert.Equal(0L, cache.GetStats().BuildsTotal); // came from file, not baker
+ // Every chunk should be resident — no further queries needed.
+ Assert.Equal(coords.Length, cache.GetStats().ResidentChunks);
+ Assert.Equal(0L, cache.GetStats().BuildsTotal); // came from file, not baker
- // Subsequent query is a clean Hit, not a Miss_NotBuilt.
- var lookup = cache.TryGetMask(map, coords[0].Item1, coords[0].Item2, sourceZ: 10);
- Assert.Equal(CacheHitKind.Hit, lookup.HitKind);
- Assert.Equal(coords.Length, cache.GetStats().ResidentChunks);
- }
- finally
- {
- cache.PreloadOnLazyOpen = false;
- }
+ // Subsequent query is a clean Hit, not a Miss_NotBuilt.
+ var lookup = cache.TryGetMask(map, coords[0].Item1, coords[0].Item2, sourceZ: 10);
+ Assert.Equal(CacheHitKind.Hit, lookup.HitKind);
+ Assert.Equal(coords.Length, cache.GetStats().ResidentChunks);
}
finally
{
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFingerprintTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFingerprintTests.cs
index dee698e59..6d2bc6f7a 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFingerprintTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFingerprintTests.cs
@@ -15,9 +15,12 @@ public class StepCacheFingerprintTests
/// different one, marking the bake stale and re-baking on every boot. Hashing the file makes
/// the fingerprint a pure function of the client's tile data, immune to those mutations.
///
- [Fact]
+ [SkippableFact]
public void Fingerprint_IgnoresRuntimeTileDataMutation()
{
+ // Without tiledata.mul there is no file to hash, so the fingerprint is constant either way.
+ TileDataRequirement.SkipIfMissing();
+
const int mapId = 1; // Trammel — loaded by the test bootstrap.
var before = StepCacheFile.ComputeFingerprint(mapId);
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs
index 9c410efc7..fd90aedf9 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs
@@ -17,11 +17,9 @@ namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
public class StepCacheLifecycleTests : IDisposable
{
- // The window is a knob on the singleton and Clear() leaves it alone, so a test that narrows
- // it would otherwise leak the narrow window into every later test in the collection.
- private readonly uint _savedWindowMs = StepCache.Instance.MissPromotionWindowMs;
+ private readonly StepCacheStateScope _cacheState = new();
- public void Dispose() => StepCache.Instance.MissPromotionWindowMs = _savedWindowMs;
+ public void Dispose() => _cacheState.Dispose();
/// Resets to a known state and returns the singleton.
private static StepCache FreshCache(int promotionThreshold)
@@ -396,26 +394,19 @@ public class StepCacheLifecycleTests : IDisposable
var cache = FreshCache(promotionThreshold: 1);
cache.MaxResidentChunks = 4;
- try
+ var map = TestMap;
+
+ // Five chunks into a cache that holds four. Which one goes doesn't matter here, only that
+ // the count comes down and the key list stays in step, so equal touch ticks are fine.
+ for (var i = 0; i < 5; i++)
{
- var map = TestMap;
-
- // Five chunks into a cache that holds four.
- for (var i = 0; i < 5; i++)
- {
- cache.TryGetMask(map, PlainX + i * 16, PlainY, sourceZ: 10);
- Thread.Sleep(2); // separate their LastTouchedTicks so LRU has something to order by
- }
-
- cache.EnforceLruCap();
-
- Assert.Equal(4, cache.GetStats().ResidentChunks);
- Assert.True(cache.GetStats().EvictionsByLruCap >= 1L);
- Assert.True(cache.ResidentIndexInSync(), "eviction desynced the key list from the resident set");
- }
- finally
- {
- cache.MaxResidentChunks = 8192;
+ cache.TryGetMask(map, PlainX + i * 16, PlainY, sourceZ: 10);
}
+
+ cache.EnforceLruCap();
+
+ Assert.Equal(4, cache.GetStats().ResidentChunks);
+ Assert.True(cache.GetStats().EvictionsByLruCap >= 1L);
+ Assert.True(cache.ResidentIndexInSync(), "eviction desynced the key list from the resident set");
}
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs
index b38c15277..769945af1 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs
@@ -20,12 +20,15 @@ namespace Server.Tests.Pathfinding;
/// falls through on everything agrees with the slow path perfectly and is worthless.
///
[Collection("Sequential Pathfinding Tests")]
-public class StepCacheParityTests
+public class StepCacheParityTests : IDisposable
{
private readonly ITestOutputHelper _output;
+ private readonly StepCacheStateScope _cacheState = new();
public StepCacheParityTests(ITestOutputHelper output) => _output = output;
+ public void Dispose() => _cacheState.Dispose();
+
// ---- layer 1: the bake agrees with MovementImpl ----
///
@@ -43,7 +46,7 @@ public class StepCacheParityTests
var map = TestMap;
Assert.NotNull(map);
- var walker = new StaticWalker();
+ var walker = new WalkerStub();
walker.MoveToWorld(new Point3D(xStart, yStart, 0), map);
var disagreements = 0;
@@ -154,12 +157,16 @@ public class StepCacheParityTests
/// the land average instead would trip the source-Z guard on raised terrain (a causeway, a
/// walkway) and report a fallthrough that is correct behaviour rather than a fault.
///
- [Theory]
+ [SkippableTheory]
[InlineData("britain_inn_dense", 1480, 1610, 32)]
[InlineData("trammel_open_plain", 1500, 1600, 32)]
[InlineData("britain_causeway", 1475, 1641, 32)]
public void CacheMatchesProbe(string label, int xStart, int yStart, int size)
{
+ // A storage fault (crossed cell index, swapped Z array) only shows against varied terrain;
+ // on a blank map every cell bakes the same answer.
+ TileDataRequirement.SkipIfMissing();
+
var cache = StepCache.Instance;
cache.Clear();
cache.MissPromotionThreshold = 1; // build on first touch: every cell should get a real answer
@@ -253,7 +260,7 @@ public class StepCacheParityTests
/// Cardinals only: the cache stores raw masks and applies the corner-cut at query time, so a
/// raw diagonal bit legitimately differs from MovementImpl's diagonal answer.
///
- [Theory]
+ [SkippableTheory]
// Seeds chosen for the terrain classes the standable-surface bake has to get right. Each one
// floods across a wide local area, so a handful covers thousands of states without a map walk.
[InlineData("brit_sewer_walkway", 6034, 1476, 5, 2500)] // static walkway over impassable land
@@ -262,6 +269,8 @@ public class StepCacheParityTests
[InlineData("trammel_open_plain", 1500, 1600, 10, 2500)] // flat ground: catches clearance false-positives
public void CacheServesReachableWalkStates(string label, int sx, int sy, int sz, int maxStates)
{
+ TileDataRequirement.SkipIfMissing();
+
var cache = StepCache.Instance;
cache.Clear();
cache.MissPromotionThreshold = 1; // build on first touch: every reached state should be answered
@@ -269,7 +278,7 @@ public class StepCacheParityTests
var map = TestMap;
Assert.NotNull(map);
- var walker = new StaticWalker();
+ var walker = new WalkerStub();
walker.MoveToWorld(new Point3D(sx, sy, sz), map);
var startIsWalkable = false;
diff --git a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/AllSpawnFilesLoadTests.cs b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/AllSpawnFilesLoadTests.cs
index 9eb9e1909..946e83e36 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/AllSpawnFilesLoadTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/AllSpawnFilesLoadTests.cs
@@ -20,10 +20,9 @@ public class AllSpawnFilesLoadTests
public void EverySpawnFile_DeserializesAndBuildsSpawners()
{
var root = Path.Combine(Core.BaseDirectory, "Data", "Spawns");
- if (!Directory.Exists(root))
- {
- return; // distribution data not present in this checkout
- }
+ // Data/ is copied into the test output by the build; a missing folder means the build was run
+ // without SolutionDir, and passing would hide that nothing was checked.
+ Assert.True(Directory.Exists(root), $"Spawn data not found at {root}");
var failures = new List();
var fileCount = 0;
@@ -64,6 +63,7 @@ public class AllSpawnFilesLoadTests
}
}
+ Assert.NotEqual(0, fileCount);
Assert.True(
failures.Count == 0,
$"Loaded {fileCount} files, built {spawnerCount} spawners. Failures:\n{string.Join("\n", failures)}"
diff --git a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/ImportCleanupTests.cs b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/ImportCleanupTests.cs
index 6b4bba2dd..e30cc289d 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/ImportCleanupTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/ImportCleanupTests.cs
@@ -3,22 +3,24 @@ using System.Collections.Generic;
using System.IO;
using Server;
using Server.Engines.Spawners;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Engines.Spawners.Json;
[Collection("Sequential UOContent Tests")]
-public class ImportCleanupTests
+public class ImportCleanupTests : IDisposable
{
private const string SpawnerGuid = "11111111-1111-1111-1111-111111111111";
+ private readonly TempDirectory _dir = new("muo-spawner-import");
+
+ public void Dispose() => _dir.Dispose();
+
[Fact]
public void Import_ValidFile_PlacesSpawner()
{
- var dir = Path.Combine(Path.GetTempPath(), "muo-spawner-import-" + Guid.NewGuid().ToString("N"));
- Directory.CreateDirectory(dir);
- var path = Path.Combine(dir, "test.json");
- File.WriteAllText(path, """
+ var path = _dir.WriteFile("test.json", """
[
{
"$type": "Spawner",
@@ -53,22 +55,15 @@ public class ImportCleanupTests
finally
{
placedSpawner?.Delete();
- if (Directory.Exists(dir))
- {
- Directory.Delete(dir, true);
- }
}
}
+ // A DTO with no map entry deserializes with map == null. The importer rejects it before building
+ // a spawner, so nothing is registered and nothing lands on any live map.
[Fact]
- public void Import_NoMap_DeletesOrphanedSpawner()
+ public void Import_NoMap_SkipsTheSpawner()
{
- // A spawner with no map entry deserialized by STJ will have map == null (or Map.Internal).
- // Verify it does NOT end up on any live map.
- var dir = Path.Combine(Path.GetTempPath(), "muo-spawner-import-nomap-" + Guid.NewGuid().ToString("N"));
- Directory.CreateDirectory(dir);
- var path = Path.Combine(dir, "test-nomap.json");
- File.WriteAllText(path, """
+ var path = _dir.WriteFile("test-nomap.json", """
[
{
"$type": "Spawner",
@@ -81,45 +76,28 @@ public class ImportCleanupTests
]
""");
- try
- {
- var all = new Dictionary();
- ImportSpawnersCommand.ImportFile(new FileInfo(path), all);
+ var guid = new Guid("22222222-2222-2222-2222-222222222222");
+ var all = new Dictionary();
+ ImportSpawnersCommand.ImportFile(new FileInfo(path), all);
- // The spawner must NOT be in allSpawners (rejected due to missing map).
- Assert.DoesNotContain(new Guid("22222222-2222-2222-2222-222222222222"), all.Keys);
- }
- finally
+ Assert.DoesNotContain(guid, all.Keys);
+
+ var maps = Map.AllMaps;
+ for (var i = 0; i < maps.Count; i++)
{
- if (Directory.Exists(dir))
+ foreach (var spawner in maps[i].GetItemsAt(new Point3D(400, 400, 0)))
{
- Directory.Delete(dir, true);
+ Assert.NotEqual(guid, spawner.Guid);
}
}
}
- private static int CountSpawnersNear(Point3D p)
- {
- var n = 0;
- foreach (var _ in Map.Felucca.GetItemsAt(p))
- {
- n++;
- }
-
- return n;
- }
-
[Fact]
public void Import_DuplicateLocation_ReplacesExistingSpawner()
{
// Verifies that an existing spawner at the same location and type is removed and replaced.
- var dir = Path.Combine(Path.GetTempPath(), "muo-spawner-import-dup-" + Guid.NewGuid().ToString("N"));
- Directory.CreateDirectory(dir);
- var path = Path.Combine(dir, "test-dup.json");
-
var newGuid = new Guid("33333333-3333-3333-3333-333333333333");
- // Use a non-interpolated raw string literal; the guid is a known constant so hardcode it.
- File.WriteAllText(path, """
+ var path = _dir.WriteFile("test-dup.json", """
[
{
"$type": "Spawner",
@@ -133,13 +111,13 @@ public class ImportCleanupTests
]
""");
- // Place a pre-existing spawner at the same location.
var existing = new Spawner(1, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10));
- existing.MoveToWorld(new Point3D(310, 310, 0), Map.Felucca);
-
BaseSpawner placedSpawner = null;
+
try
{
+ existing.MoveToWorld(new Point3D(310, 310, 0), Map.Felucca);
+
var all = new Dictionary { [existing.Guid] = existing };
ImportSpawnersCommand.ImportFile(new FileInfo(path), all);
@@ -160,15 +138,8 @@ public class ImportCleanupTests
}
finally
{
- if (!existing.Deleted)
- {
- existing.Delete();
- }
+ existing.Delete();
placedSpawner?.Delete();
- if (Directory.Exists(dir))
- {
- Directory.Delete(dir, true);
- }
}
}
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/SpawnerDtoEntryTests.cs b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/SpawnerDtoEntryTests.cs
index 8a25c4b0f..930f059ce 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/SpawnerDtoEntryTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/SpawnerDtoEntryTests.cs
@@ -15,15 +15,21 @@ public class SpawnerDtoEntryTests
public void Export_WritesEntriesAtSameJsonPosition_WithDisabledOnlyWhenSet()
{
var spawner = new Spawner(1, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10), 0, default, "Rabbit", "Bird");
- spawner.Entries[1].Disabled = true;
- var json = SpawnerJsonSerializer.SerializeCompact(new List { spawner.ToDto() });
+ try
+ {
+ spawner.Entries[1].Disabled = true;
- Assert.Contains("\"entries\": [\n { \"name\": \"Rabbit\"", json);
- Assert.Contains("{ \"name\": \"Bird\"", json);
- Assert.Equal(1, CountOccurrences(json, "\"disabled\": true"));
+ var json = SpawnerJsonSerializer.SerializeCompact(new List { spawner.ToDto() });
- spawner.Delete();
+ Assert.Contains("\"entries\": [\n { \"name\": \"Rabbit\"", json);
+ Assert.Contains("{ \"name\": \"Bird\"", json);
+ Assert.Equal(1, CountOccurrences(json, "\"disabled\": true"));
+ }
+ finally
+ {
+ spawner.Delete();
+ }
}
[Fact]
@@ -35,11 +41,16 @@ public class SpawnerDtoEntryTests
var dtos = JsonSerializer.Deserialize>(json, SpawnerJsonSerializer.Options);
var spawner = dtos![0].ToSpawner();
- Assert.Single(spawner.Entries);
- Assert.True(spawner.Entries[0].Disabled);
- Assert.Same(dtos[0].EntryView[0], spawner.Entries[0]);
-
- spawner.Delete();
+ try
+ {
+ Assert.Single(spawner.Entries);
+ Assert.True(spawner.Entries[0].Disabled);
+ Assert.Same(dtos[0].EntryView[0], spawner.Entries[0]);
+ }
+ finally
+ {
+ spawner.Delete();
+ }
}
private static int CountOccurrences(string source, string value)
diff --git a/Projects/UOContent.Tests/Tests/Engines/Spawners/SectorSpawnCacheTests.cs b/Projects/UOContent.Tests/Tests/Engines/Spawners/SectorSpawnCacheTests.cs
index 1acc29960..ad67e96b9 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Spawners/SectorSpawnCacheTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Spawners/SectorSpawnCacheTests.cs
@@ -260,14 +260,8 @@ public class SectorSpawnCacheManagerTests
}
}
-[Collection("SectorSpawnCache")]
public class SpiralScanTests
{
- public SpiralScanTests()
- {
- SectorSpawnCacheManager.ClearAll();
- }
-
[Theory]
[InlineData(1, 0, -1, -1)] // Ring 1, position 0: top-left
[InlineData(1, 1, 0, -1)] // Ring 1, position 1: top
diff --git a/Projects/UOContent.Tests/Tests/Engines/Spawners/SpawnerEntryOwnershipTests.cs b/Projects/UOContent.Tests/Tests/Engines/Spawners/SpawnerEntryOwnershipTests.cs
index 96023870d..6d9e8e711 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Spawners/SpawnerEntryOwnershipTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Spawners/SpawnerEntryOwnershipTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
@@ -11,26 +12,43 @@ using Xunit;
namespace UOContent.Tests.Engines.Spawners;
[Collection("Sequential UOContent Tests")]
-public class SpawnerEntryOwnershipTests
+public class SpawnerEntryOwnershipTests : IDisposable
{
+ // Deleted in Dispose so a failed assertion cannot leave spawners registered in the world.
+ private readonly List- _created = [];
+
+ public void Dispose()
+ {
+ for (var i = 0; i < _created.Count; i++)
+ {
+ _created[i].Delete();
+ }
+ }
+
+ private T Track(T item) where T : Item
+ {
+ _created.Add(item);
+ return item;
+ }
+
+ private Spawner NewSpawner(params string[] names) =>
+ Track(new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, names));
+
[Fact]
public void Disabled_DefaultsFalse_AndRoundTripsBinary()
{
- var spawner = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit", "Bird");
+ var spawner = NewSpawner("Rabbit", "Bird");
Assert.False(spawner.Entries[0].Disabled);
Assert.True(spawner.Entries[0].Enabled);
spawner.Entries[1].Disabled = true;
var bytes = SpawnerBlob.Write(spawner);
- var loaded = SpawnerBlob.Read(bytes, (Serial)0x40001234);
+ var loaded = Track(SpawnerBlob.Read(bytes, (Serial)0x40001234));
Assert.Equal(2, loaded.Entries.Count);
Assert.False(loaded.Entries[0].Disabled);
Assert.True(loaded.Entries[1].Disabled);
-
- spawner.Delete();
- loaded.Delete();
}
[Fact]
@@ -48,7 +66,7 @@ public class SpawnerEntryOwnershipTests
[Fact]
public void AddRemoveClear_OperateOnOwnerList()
{
- var spawner = new Spawner();
+ var spawner = Track(new Spawner());
Assert.Empty(spawner.Entries);
var a = spawner.AddEntry("Rabbit", 100, 2, false);
@@ -63,17 +81,15 @@ public class SpawnerEntryOwnershipTests
spawner.RemoveAllEntries();
Assert.Empty(spawner.Entries);
-
- spawner.Delete();
}
[Fact]
public void Dupe_ClonesEntriesIntoIndependentList()
{
- var source = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit", "Bird");
+ var source = NewSpawner("Rabbit", "Bird");
source.Entries[1].Disabled = true;
- var copy = new Spawner();
+ var copy = Track(new Spawner());
source.Dupe(copy);
Assert.Equal(2, copy.Entries.Count);
@@ -84,31 +100,25 @@ public class SpawnerEntryOwnershipTests
source.AddEntry("Orc", 100, 1, false);
Assert.Equal(2, copy.Entries.Count);
-
- source.Delete();
- copy.Delete();
}
[Fact]
public void CopyEntriesTo_ReplacesTargetEntries()
{
- var source = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit");
- var target = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Orc", "Troll");
+ var source = NewSpawner("Rabbit");
+ var target = NewSpawner("Orc", "Troll");
source.CopyEntriesTo(target);
Assert.Single(target.Entries);
Assert.Equal("Rabbit", target.Entries[0].SpawnedName);
Assert.NotSame(source.Entries[0], target.Entries[0]);
-
- source.Delete();
- target.Delete();
}
[Fact]
public void CopyEntriesTo_Self_IsNoOp()
{
- var spawner = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit", "Bird");
+ var spawner = NewSpawner("Rabbit", "Bird");
var first = spawner.Entries[0];
var second = spawner.Entries[1];
@@ -117,18 +127,16 @@ public class SpawnerEntryOwnershipTests
Assert.Equal(2, spawner.Entries.Count);
Assert.Same(first, spawner.Entries[0]);
Assert.Same(second, spawner.Entries[1]);
-
- spawner.Delete();
}
[Fact]
public void RemoveEntry_ForeignEntry_IsIgnored()
{
- var a = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit");
- var b = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Bird");
+ var a = NewSpawner("Rabbit");
+ var b = NewSpawner("Bird");
var foreign = b.Entries[0];
- var spawned = new Item(0x1f13);
+ var spawned = Track(new Item(0x1f13));
foreign.AddToSpawned(spawned);
a.RemoveEntry(foreign);
@@ -140,10 +148,6 @@ public class SpawnerEntryOwnershipTests
Assert.False(spawned.Deleted);
Assert.Single(foreign.Spawned);
-
- spawned.Delete();
- a.Delete();
- b.Delete();
}
// Manual: remove the Skip, run, and read the numbers from the thrown assertion.
diff --git a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseChildPublicTests.cs b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseChildPublicTests.cs
index 01a58a697..ca3c7399f 100644
--- a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseChildPublicTests.cs
+++ b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseChildPublicTests.cs
@@ -17,30 +17,42 @@ public class CorpseChildPublicTests
private static VikingSword EquipSword(Mobile owner)
{
var sword = new VikingSword { Layer = Layer.OneHanded };
- Assert.True(owner.EquipItem(sword));
+ var equipped = owner.EquipItem(sword);
+ if (!equipped)
+ {
+ sword.Delete();
+ }
+
+ Assert.True(equipped);
return sword;
}
+ private static Mobile NewOwner(int body)
+ {
+ var owner = new Mobile(World.NewMobile);
+ owner.DefaultMobileInit();
+ owner.Body = body;
+ return owner;
+ }
+
[Fact]
public void IsChildPublic_HumanCorpseWithWornGear_ReturnsTrue()
{
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
-
- var wornItem = EquipSword(owner);
-
- var corpse = new Corpse(owner, owner.Items);
+ var owner = NewOwner(0x190);
+ Corpse corpse = null;
try
{
+ var wornItem = EquipSword(owner);
+ corpse = new Corpse(owner, owner.Items);
+
Assert.Equal(0x2006, corpse.ItemID);
Assert.True(((Body)corpse.Amount).IsHuman);
Assert.True(corpse.IsChildPublic(wornItem));
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
}
}
@@ -48,26 +60,24 @@ public class CorpseChildPublicTests
[Fact]
public void IsChildPublic_HumanCorpseWithLootItem_ReturnsFalse()
{
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
-
- var wornItem = EquipSword(owner);
-
- var corpse = new Corpse(owner, owner.Items);
-
- var lootItem = new Gold(100);
- corpse.DropItem(lootItem);
+ var owner = NewOwner(0x190);
+ Corpse corpse = null;
try
{
+ EquipSword(owner);
+ corpse = new Corpse(owner, owner.Items);
+
+ var lootItem = new Gold(100);
+ corpse.DropItem(lootItem);
+
Assert.Equal(0x2006, corpse.ItemID);
Assert.True(((Body)corpse.Amount).IsHuman);
Assert.False(corpse.IsChildPublic(lootItem));
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
}
}
@@ -75,16 +85,14 @@ public class CorpseChildPublicTests
[Fact]
public void IsChildPublic_BoneCorpse_ReturnsFalse()
{
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
-
- var wornItem = EquipSword(owner);
-
- var corpse = new Corpse(owner, owner.Items);
+ var owner = NewOwner(0x190);
+ Corpse corpse = null;
try
{
+ var wornItem = EquipSword(owner);
+ corpse = new Corpse(owner, owner.Items);
+
corpse.ItemID = 0xECA;
Assert.True(((Body)corpse.Amount).IsHuman);
@@ -93,7 +101,7 @@ public class CorpseChildPublicTests
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
}
}
@@ -101,23 +109,21 @@ public class CorpseChildPublicTests
[Fact]
public void IsChildPublic_NonHumanCorpse_ReturnsFalse()
{
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0xC9;
-
- var wornItem = EquipSword(owner);
-
- var corpse = new Corpse(owner, owner.Items);
+ var owner = NewOwner(0xC9);
+ Corpse corpse = null;
try
{
+ var wornItem = EquipSword(owner);
+ corpse = new Corpse(owner, owner.Items);
+
Assert.Equal(0x2006, corpse.ItemID);
Assert.False(((Body)corpse.Amount).IsHuman);
Assert.False(corpse.IsChildPublic(wornItem));
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
}
}
@@ -129,36 +135,48 @@ public class CorpseChildPublicTests
return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
}
+ private static Mobile NewBystander(NetState ns, Point3D location)
+ {
+ ns.Account = new MockAccount();
+ var bystander = new Mobile(World.NewMobile);
+ bystander.DefaultMobileInit();
+ ns.Mobile = bystander;
+ bystander.NetState = ns;
+ bystander.MoveToWorld(location, Map.Felucca);
+ return bystander;
+ }
+
+ private static void DeleteBystander(NetState ns, Mobile bystander)
+ {
+ ns.Mobile = null;
+ ns.Dispose();
+ bystander?.Delete();
+ }
+
[Fact]
public void HumanCorpse_WornGearRemovalBroadcasts_LootRemovalDoesNot()
{
var corpseLoc = new Point3D(1500, 1500, 0);
var bystanderNs = PacketTestUtilities.CreateTestNetState();
- bystanderNs.Account = new MockAccount();
- var bystander = new Mobile(World.NewMobile);
- bystander.DefaultMobileInit();
- bystanderNs.Mobile = bystander;
- bystander.NetState = bystanderNs;
- bystander.MoveToWorld(new Point3D(corpseLoc.X + 1, corpseLoc.Y, corpseLoc.Z), Map.Felucca);
-
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
-
- var sword = new VikingSword();
- var corpse = new Corpse(owner, [sword]);
- corpse.AddItem(sword);
-
- var lootItem = new Gold(100);
- corpse.DropItem(lootItem);
-
- corpse.MoveToWorld(corpseLoc, Map.Felucca);
-
+ Mobile bystander = null;
+ var owner = NewOwner(0x190);
+ Corpse corpse = null;
Container pouch = null;
try
{
+ bystander = NewBystander(bystanderNs, new Point3D(corpseLoc.X + 1, corpseLoc.Y, corpseLoc.Z));
+
+ var sword = new VikingSword();
+ corpse = new Corpse(owner, [sword]);
+ corpse.AddItem(sword);
+
+ var lootItem = new Gold(100);
+ corpse.DropItem(lootItem);
+
+ corpse.MoveToWorld(corpseLoc, Map.Felucca);
+
// A container not in the world is a valid destination; only the removal from the corpse matters here.
pouch = new Container(0xE75);
pouch.AddItem(sword);
@@ -171,11 +189,9 @@ public class CorpseChildPublicTests
finally
{
pouch?.Delete();
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
- bystanderNs.Mobile = null;
- bystanderNs.Dispose();
- bystander.Delete();
+ DeleteBystander(bystanderNs, bystander);
}
}
@@ -185,25 +201,20 @@ public class CorpseChildPublicTests
var corpseLoc = new Point3D(1500, 1500, 0);
var bystanderNs = PacketTestUtilities.CreateTestNetState();
- bystanderNs.Account = new MockAccount();
- var bystander = new Mobile(World.NewMobile);
- bystander.DefaultMobileInit();
- bystanderNs.Mobile = bystander;
- bystander.NetState = bystanderNs;
- bystander.MoveToWorld(new Point3D(corpseLoc.X + 1, corpseLoc.Y, corpseLoc.Z), Map.Felucca);
-
- var owner = new Mobile(World.NewMobile);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
- owner.MoveToWorld(new Point3D(corpseLoc.X - 1, corpseLoc.Y, corpseLoc.Z), Map.Felucca);
-
+ Mobile bystander = null;
+ var owner = NewOwner(0x190);
var sword = new VikingSword { Layer = Layer.OneHanded };
- var corpse = new Corpse(owner, [sword]);
- corpse.AddItem(sword);
- corpse.MoveToWorld(corpseLoc, Map.Felucca);
+ Corpse corpse = null;
try
{
+ bystander = NewBystander(bystanderNs, new Point3D(corpseLoc.X + 1, corpseLoc.Y, corpseLoc.Z));
+ owner.MoveToWorld(new Point3D(corpseLoc.X - 1, corpseLoc.Y, corpseLoc.Z), Map.Felucca);
+
+ corpse = new Corpse(owner, [sword]);
+ corpse.AddItem(sword);
+ corpse.MoveToWorld(corpseLoc, Map.Felucca);
+
// Lifting takes the piece off the worn list before the remove is sent; the remove must
// still reach everyone who was shown it worn.
owner.Lift(sword, sword.Amount, out var rejected, out _);
@@ -216,11 +227,9 @@ public class CorpseChildPublicTests
{
owner.Holding = null;
sword.Delete();
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
- bystanderNs.Mobile = null;
- bystanderNs.Dispose();
- bystander.Delete();
+ DeleteBystander(bystanderNs, bystander);
}
}
}
diff --git a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseLootTests.cs b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseLootTests.cs
index 26556155e..9cfec050a 100644
--- a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseLootTests.cs
+++ b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseLootTests.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using Server;
using Server.Items;
using Xunit;
@@ -11,19 +10,22 @@ public class CorpseLootTests
[Fact]
public void LiftedWornGear_PutBack_StaysLoot()
{
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
-
- // Weapons take their layer from tiledata, which CI doesn't load.
- var sword = new VikingSword { Layer = Layer.OneHanded };
- Assert.True(owner.EquipItem(sword));
-
- var corpse = new Corpse(owner, new List
- { sword });
- corpse.DropItem(sword);
+ var owner = new Mobile(World.NewMobile);
+ VikingSword sword = null;
+ Corpse corpse = null;
try
{
+ owner.DefaultMobileInit();
+ owner.Body = 0x190;
+
+ // Weapons take their layer from tiledata, which CI doesn't load.
+ sword = new VikingSword { Layer = Layer.OneHanded };
+ Assert.True(owner.EquipItem(sword));
+
+ corpse = new Corpse(owner, [sword]);
+ corpse.DropItem(sword);
+
Assert.Contains(sword, corpse.EquipItems);
corpse.OnItemLifted(owner, sword);
@@ -35,7 +37,8 @@ public class CorpseLootTests
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
+ sword?.Delete();
owner.Delete();
}
}
diff --git a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs
index b5cec8aa6..1669147f2 100644
--- a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs
+++ b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs
@@ -10,19 +10,34 @@ namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class CorpsePacketTests
{
+ // Weapons take their layer from tiledata, which CI doesn't load; without an explicit layer the
+ // equip is refused and the packets would only cover an empty item list.
+ private static VikingSword EquipSword(Mobile m)
+ {
+ var sword = new VikingSword { Layer = Layer.OneHanded };
+ var equipped = m.EquipItem(sword);
+ if (!equipped)
+ {
+ sword.Delete();
+ }
+
+ Assert.True(equipped);
+ return sword;
+ }
+
[Fact]
public void TestCorpseEquipPacket()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
-
- var weapon = new VikingSword();
- m.EquipItem(weapon);
-
- var c = new Corpse(m, m.Items);
+ var m = new Mobile(World.NewMobile);
+ VikingSword weapon = null;
+ Corpse c = null;
try
{
+ m.DefaultMobileInit();
+ weapon = EquipSword(m);
+ c = new Corpse(m, m.Items);
+
var expected = new CorpseEquip(m, c).Compile();
using var ns = PacketTestUtilities.CreateTestNetState();
@@ -33,8 +48,9 @@ public class CorpsePacketTests
}
finally
{
- c.Delete();
- weapon.Delete();
+ c?.Delete();
+ weapon?.Delete();
+ m.Delete();
}
}
@@ -43,16 +59,16 @@ public class CorpsePacketTests
[InlineData(ProtocolChanges.ContainerGridLines)]
public void TestCorpseContainerPacket(ProtocolChanges changes)
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
-
- var weapon = new VikingSword();
- m.EquipItem(weapon);
-
- var c = new Corpse(m, m.Items);
+ var m = new Mobile(World.NewMobile);
+ VikingSword weapon = null;
+ Corpse c = null;
try
{
+ m.DefaultMobileInit();
+ weapon = EquipSword(m);
+ c = new Corpse(m, m.Items);
+
using var ns = PacketTestUtilities.CreateTestNetState();
ns.ProtocolChanges = changes;
@@ -65,8 +81,9 @@ public class CorpsePacketTests
}
finally
{
- c.Delete();
- weapon.Delete();
+ c?.Delete();
+ weapon?.Delete();
+ m.Delete();
}
}
}
diff --git a/Projects/UOContent.Tests/Tests/Items/VendorBuybackPackTests.cs b/Projects/UOContent.Tests/Tests/Items/VendorBuybackPackTests.cs
index cc0ccad42..b9f13a2f1 100644
--- a/Projects/UOContent.Tests/Tests/Items/VendorBuybackPackTests.cs
+++ b/Projects/UOContent.Tests/Tests/Items/VendorBuybackPackTests.cs
@@ -87,14 +87,14 @@ public class VendorBuybackPackTests
var pack = new VendorBuybackPack();
var items = new Item[10];
- for (var i = 0; i < items.Length; i++)
- {
- items[i] = new Item(0x1234);
- pack.AddBuyback(items[i], 250);
- }
-
try
{
+ for (var i = 0; i < items.Length; i++)
+ {
+ items[i] = new Item(0x1234);
+ pack.AddBuyback(items[i], 250);
+ }
+
pack.Purge();
Assert.Empty(pack.Items);
diff --git a/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs b/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs
index da896ba8b..745922d08 100644
--- a/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs
+++ b/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs
@@ -10,13 +10,19 @@ namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class ThrowingTests
{
- // Exposes protected ModifyHitChance so the math can be verified directly.
+ // Exposes protected ModifyHitChance so the math can be verified directly. The requirement and
+ // throw band are pinned here so retuning a shipped weapon cannot move these expectations.
private class TestThrown : BaseThrown
{
- public TestThrown() : base(0x8FF) { }
+ // Weapons take their layer from tiledata, which CI doesn't load; without it AddItem is refused.
+ public TestThrown() : base(0x8FF) => Layer = Layer.OneHanded;
+
+ public override int AosStrengthReq => 60;
public override int MinThrowRange => 4;
+ public override int MaxThrowRange => 7;
+
public double TestModifyHitChance(Mobile attacker, Mobile defender, double chance) =>
ModifyHitChance(attacker, defender, chance);
@@ -220,18 +226,19 @@ public class ThrowingTests
}
}
- // DefMaxRange STR scaling (uses SoulGlaive: StrReq 60, Min 8, Max 11)
+ // DefMaxRange STR scaling (TestThrown: StrReq 60, Min 4, Max 7, so one tile per 26 Str)
[Fact]
public void DefMaxRange_AtStrReq_EqualsMinThrowRange()
{
- var weapon = new SoulGlaive();
+ var weapon = new TestThrown();
var attacker = CreateMobile(Map.Felucca, new Point3D(5800, 500, 0));
try
{
attacker.RawStr = 60; // == AosStrengthReq
attacker.AddItem(weapon);
- Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 8
+ Assert.Same(attacker, weapon.Parent);
+ Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 4
}
finally
{
@@ -243,13 +250,14 @@ public class ThrowingTests
[Fact]
public void DefMaxRange_At140Str_EqualsMaxThrowRange()
{
- var weapon = new SoulGlaive();
+ var weapon = new TestThrown();
var attacker = CreateMobile(Map.Felucca, new Point3D(5810, 500, 0));
try
{
attacker.RawStr = 140;
attacker.AddItem(weapon);
- Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 11
+ Assert.Same(attacker, weapon.Parent);
+ Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 7
}
finally
{
@@ -261,13 +269,14 @@ public class ThrowingTests
[Fact]
public void DefMaxRange_AboveMaxStr_IsCappedAtMaxThrowRange()
{
- var weapon = new SoulGlaive();
+ var weapon = new TestThrown();
var attacker = CreateMobile(Map.Felucca, new Point3D(5820, 500, 0));
try
{
- attacker.RawStr = 200; // uncapped formula would give 13
+ attacker.RawStr = 200; // uncapped formula would give 9
attacker.AddItem(weapon);
- Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 11, not 13
+ Assert.Same(attacker, weapon.Parent);
+ Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 7, not 9
}
finally
{
@@ -279,13 +288,14 @@ public class ThrowingTests
[Fact]
public void DefMaxRange_BelowStrReq_FlooredAtMinThrowRange()
{
- var weapon = new SoulGlaive();
+ var weapon = new TestThrown();
var attacker = CreateMobile(Map.Felucca, new Point3D(5830, 500, 0));
try
{
attacker.RawStr = 10; // below StrReq 60
attacker.AddItem(weapon);
- Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 8, not 6
+ Assert.Same(attacker, weapon.Parent);
+ Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 4, not 3
}
finally
{
@@ -305,6 +315,7 @@ public class ThrowingTests
{
attacker.RawStr = 140; // MaxRange == MaxThrowRange == 7
attacker.AddItem(weapon);
+ Assert.Same(attacker, weapon.Parent);
var defender = CreateMobile(map, new Point3D(5907, 500, 0)); // distance 7 == MaxRange
try
{
@@ -333,6 +344,7 @@ public class ThrowingTests
{
attacker.RawStr = 140; // MaxRange 7
attacker.AddItem(weapon);
+ Assert.Same(attacker, weapon.Parent);
var defender = CreateMobile(map, new Point3D(5925, 500, 0)); // distance 5 < 7
try
{
diff --git a/Projects/UOContent.Tests/Tests/Misc/CorpseNotorietyTests.cs b/Projects/UOContent.Tests/Tests/Misc/CorpseNotorietyTests.cs
index a46c56003..68be84e7b 100644
--- a/Projects/UOContent.Tests/Tests/Misc/CorpseNotorietyTests.cs
+++ b/Projects/UOContent.Tests/Tests/Misc/CorpseNotorietyTests.cs
@@ -17,22 +17,23 @@ public class CorpseNotorietyTests
public void PlayerCorpseNotorietyIgnoresTransformedBody(int bodyMod, int expected, int kills = 0)
{
var victim = new PlayerMobile(World.NewMobile);
- victim.DefaultMobileInit();
- victim.Kills = kills;
- victim.BodyMod = bodyMod;
-
var looter = new PlayerMobile(World.NewMobile);
- looter.DefaultMobileInit();
-
- var corpse = new Corpse(victim, []);
+ Corpse corpse = null;
try
{
+ victim.DefaultMobileInit();
+ victim.Kills = kills;
+ victim.BodyMod = bodyMod;
+ looter.DefaultMobileInit();
+
+ corpse = new Corpse(victim, []);
+
Assert.Equal(expected, NotorietyHandlers.CorpseNotoriety(looter, corpse));
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
victim.Delete();
looter.Delete();
}
diff --git a/Projects/UOContent.Tests/Tests/Misc/EmailTests.cs b/Projects/UOContent.Tests/Tests/Misc/EmailTests.cs
index de6825c9f..06d940512 100644
--- a/Projects/UOContent.Tests/Tests/Misc/EmailTests.cs
+++ b/Projects/UOContent.Tests/Tests/Misc/EmailTests.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Reflection;
using Server.Configurations;
using Server.Misc;
using Xunit;
@@ -7,13 +8,22 @@ using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
-public class EmailTests
+public class EmailTests : IDisposable
{
+ private static readonly PropertyInfo _emailEnabled =
+ typeof(EmailConfiguration).GetProperty(nameof(EmailConfiguration.EmailEnabled));
+
+ private readonly bool _savedEmailEnabled = EmailConfiguration.EmailEnabled;
+
+ // Pinned off rather than read from the host's email.json, so a developer config with email
+ // enabled cannot turn these into real sends.
+ public EmailTests() => _emailEnabled.SetValue(null, false);
+
+ public void Dispose() => _emailEnabled.SetValue(null, _savedEmailEnabled);
+
[Fact]
public void SendCrashEmailDoesNothingWhileEmailIsDisabled()
{
- Assert.False(EmailConfiguration.EmailEnabled, "The test host never enables email");
-
// Missing input must be ignored while email is disabled.
var report = Path.Combine(Path.GetTempPath(), $"crash-email-test-{Guid.NewGuid():N}", "Crash.log");
@@ -23,8 +33,7 @@ public class EmailTests
[Fact]
public void SendQueueEmailDoesNothingWhileEmailIsDisabled()
{
- Assert.False(EmailConfiguration.EmailEnabled, "The test host never enables email");
-
+ // A null entry would throw on the enabled path, so returning cleanly shows the guard held.
Email.SendQueueEmail(null, "Other");
}
diff --git a/Projects/UOContent.Tests/Tests/Misc/GuildSerializePurityTests.cs b/Projects/UOContent.Tests/Tests/Misc/GuildSerializePurityTests.cs
index f596282f6..86787e0f2 100644
--- a/Projects/UOContent.Tests/Tests/Misc/GuildSerializePurityTests.cs
+++ b/Projects/UOContent.Tests/Tests/Misc/GuildSerializePurityTests.cs
@@ -22,40 +22,55 @@ public class GuildSerializePurityTests
public void SerializeDoesNotRecalculateFealtyOrMutateState()
{
var leader = CreatePlayer();
- var guild = new Guild(leader, "Purity Test Guild", "PTG");
- var staleFealty = Core.Now - TimeSpan.FromDays(3);
- guild.LastFealty = staleFealty;
+ Guild guild = null;
- var writer = new BufferWriter(true);
- guild.Serialize(writer);
+ try
+ {
+ guild = new Guild(leader, "Purity Test Guild", "PTG");
+ var staleFealty = Core.Now - TimeSpan.FromDays(3);
+ guild.LastFealty = staleFealty;
- Assert.Equal(staleFealty, guild.LastFealty);
- Assert.Same(leader, guild.Leader);
+ var writer = new BufferWriter(true);
+ guild.Serialize(writer);
- var first = writer.Buffer.AsSpan(0, (int)writer.Position).ToArray();
- writer.Seek(0, SeekOrigin.Begin);
- guild.Serialize(writer);
- var second = writer.Buffer.AsSpan(0, (int)writer.Position).ToArray();
+ Assert.Equal(staleFealty, guild.LastFealty);
+ Assert.Same(leader, guild.Leader);
- Assert.Equal(first, second);
+ var first = writer.Buffer.AsSpan(0, (int)writer.Position).ToArray();
+ writer.Seek(0, SeekOrigin.Begin);
+ guild.Serialize(writer);
+ var second = writer.Buffer.AsSpan(0, (int)writer.Position).ToArray();
- writer.Close();
- guild.Disband();
- leader.Delete();
+ Assert.Equal(first, second);
+
+ writer.Close();
+ }
+ finally
+ {
+ guild?.Disband();
+ leader.Delete();
+ }
}
[Fact]
public void RunMaintenanceRecalculatesStaleFealty()
{
var leader = CreatePlayer();
- var guild = new Guild(leader, "Maintenance Test Guild", "MTG");
- guild.LastFealty = Core.Now - TimeSpan.FromDays(3);
+ Guild guild = null;
- guild.RunMaintenance();
+ try
+ {
+ guild = new Guild(leader, "Maintenance Test Guild", "MTG");
+ guild.LastFealty = Core.Now - TimeSpan.FromDays(3);
- Assert.True(Core.Now - guild.LastFealty < TimeSpan.FromMinutes(1));
+ guild.RunMaintenance();
- guild.Disband();
- leader.Delete();
+ Assert.True(Core.Now - guild.LastFealty < TimeSpan.FromMinutes(1));
+ }
+ finally
+ {
+ guild?.Disband();
+ leader.Delete();
+ }
}
}
diff --git a/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs b/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
index 60b450b0d..233086f04 100644
--- a/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
+++ b/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
@@ -38,7 +38,7 @@ public class NameVerificationTests
[InlineData("Blacksmith 123")]
[InlineData("Baker-Smith")]
[InlineData("*Innkeeper*")]
- public void ValidateVendorName_ValidNames_ReturnsFalse(string name)
+ public void ValidateVendorName_DisallowedCharacters_ReturnsFalse(string name)
{
Assert.False(NameVerification.ValidateVendorName(name));
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/AcquisitionTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/AcquisitionTests.cs
index 3ee516dcf..e06571b63 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/AcquisitionTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/AcquisitionTests.cs
@@ -60,9 +60,14 @@ public class AcquisitionTests : IDisposable
bc.NextReacquireTime = Core.TickCount;
Assert.False(bc.AIObject.AcquireFocusMob(bc.RangePerception, FightMode.Closest, false, false, true));
- Assert.InRange(bc.NextReacquireTime - Core.TickCount, 5000, 10000);
+ Assert.Equal(ReacquireDelayMs(bc), bc.NextReacquireTime - Core.TickCount);
}
+ private static long ReacquireDelayMs(BaseCreature bc) => (long)bc.ReacquireDelay.TotalMilliseconds;
+
+ // A legal, fully armed deadline: as far out as a scan ever sets it. Movement can only pull it in.
+ private static void ArmFullDelay(BaseCreature bc) => bc.NextReacquireTime = Core.TickCount + ReacquireDelayMs(bc);
+
[Fact]
public void WedgedGate_SelfHeals()
{
@@ -78,7 +83,7 @@ public class AcquisitionTests : IDisposable
_created.Add(target);
// Illegal deadline (beyond ReacquireDelay): must read as open, not block forever.
- bc.NextReacquireTime = Core.TickCount + 60000;
+ bc.NextReacquireTime = Core.TickCount + ReacquireDelayMs(bc) * 6;
Assert.True(bc.AIObject.AcquireFocusMob(bc.RangePerception, FightMode.Closest, false, false, true));
Assert.Equal(target, bc.FocusMob);
@@ -96,7 +101,8 @@ public class AcquisitionTests : IDisposable
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var bc = Spawn(map, new Point3D(1500, 1600, (sbyte)z));
- bc.NextReacquireTime = Core.TickCount + 8000;
+ ArmFullDelay(bc);
+ var armed = bc.NextReacquireTime;
Mobile mover;
if (wildMover)
@@ -117,12 +123,12 @@ public class AcquisitionTests : IDisposable
if (notices)
{
- // Clamped to the approach delay (2s), never opened outright.
+ // Clamped to the approach delay, never opened outright.
Assert.InRange(remaining, 1, (long)bc.AcquireOnApproachDelay.TotalMilliseconds);
}
else
{
- Assert.True(remaining > 5000);
+ Assert.Equal(armed, bc.NextReacquireTime);
}
}
@@ -150,7 +156,7 @@ public class AcquisitionTests : IDisposable
bc.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map);
bc.AIObject.AITimer?.Stop();
_created.Add(bc);
- bc.NextReacquireTime = Core.TickCount + 8000;
+ ArmFullDelay(bc);
var mover = new TargetStub { Player = true };
mover.DefaultMobileInit();
@@ -173,7 +179,7 @@ public class AcquisitionTests : IDisposable
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var bc = Spawn(map, new Point3D(1500, 1600, (sbyte)z));
- bc.NextReacquireTime = Core.TickCount + 8000;
+ ArmFullDelay(bc);
var mover = new TargetStub { Player = true };
mover.DefaultMobileInit();
@@ -206,6 +212,6 @@ public class AcquisitionTests : IDisposable
Assert.True(bc.AIObject.AcquireFocusMob(bc.RangePerception, FightMode.Closest, false, false, true));
Assert.Equal(target, bc.FocusMob);
- Assert.True(bc.NextReacquireTime - Core.TickCount > 5000);
+ Assert.Equal(ReacquireDelayMs(bc), bc.NextReacquireTime - Core.TickCount);
}
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachOutcomeTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachOutcomeTests.cs
index fe8e397a1..55965375b 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachOutcomeTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachOutcomeTests.cs
@@ -1,43 +1,47 @@
+using System;
using System.Collections.Generic;
using Server;
using Server.Engines.Pathing.Cache;
using Server.Mobiles;
using Server.Tests;
-using Server.Tests.Mobiles.AI;
using Xunit;
namespace UOContent.Tests.Mobiles.AI;
// Driven manually against live map statics; the pathfinder's buffers are not reentrant.
[Collection("Sequential Pathfinding Tests")]
-public class ApproachOutcomeTests
+public class ApproachOutcomeTests : IDisposable
{
- private sealed class Stub : BaseCreature
+ private readonly List _created = [];
+
+ public void Dispose()
{
- public Stub(Serial serial) : base(serial) => Body = 0xC9;
+ for (var i = _created.Count - 1; i >= 0; i--)
+ {
+ _created[i].Delete();
+ }
+
+ _created.Clear();
}
- private sealed class Target : Mobile
- {
- public Target() => Body = 0xC9;
- }
-
- private static (Stub bc, BaseAI ai) NewFollower(Map map, int x, int y)
+ private (BareCreatureStub bc, BaseAI ai) NewFollower(Map map, int x, int y)
{
map.GetAverageZ(x, y, out _, out var z, out _);
- var bc = new Stub(World.NewMobile);
+ var bc = new BareCreatureStub(World.NewMobile);
bc.DefaultMobileInit();
bc.MoveToWorld(new Point3D(x, y, (sbyte)z), map);
+ _created.Add(bc);
BaseAI ai = new AnimalAI(bc);
ai.AITimer?.Stop();
return (bc, ai);
}
- private static Target NewTarget(Map map, int x, int y)
+ private WalkerStub NewTarget(Map map, int x, int y)
{
map.GetAverageZ(x, y, out _, out var z, out _);
- var t = new Target();
+ var t = new WalkerStub();
t.MoveToWorld(new Point3D(x, y, (sbyte)z), map);
+ _created.Add(t);
return t;
}
@@ -46,32 +50,24 @@ public class ApproachOutcomeTests
{
TileDataRequirement.SkipIfMissing();
var map = Map.Maps[1];
- var (bc, ai) = NewFollower(map, 1500, 1600);
+ var (_, ai) = NewFollower(map, 1500, 1600);
var target = NewTarget(map, 1497, 1600);
StepCache.Instance.Clear();
- try
- {
- ai.NextMove = 0;
- ai.MoveTo(target, 1);
- Assert.Equal(ApproachOutcome.DirectProgress, ai.LastApproach);
+ ai.NextMove = 0;
+ ai.MoveTo(target, 1);
+ Assert.Equal(ApproachOutcome.DirectProgress, ai.LastApproach);
- ai.MoveTo(target, 1); // budget consumed: no step
- Assert.Equal(ApproachOutcome.Waiting, ai.LastApproach);
+ ai.MoveTo(target, 1); // budget consumed: no step
+ Assert.Equal(ApproachOutcome.Waiting, ai.LastApproach);
- ai.NextMove = 0;
- ai.MoveTo(target, 1); // lands adjacent: still a progress step
- Assert.Equal(ApproachOutcome.DirectProgress, ai.LastApproach);
+ ai.NextMove = 0;
+ ai.MoveTo(target, 1); // lands adjacent: still a progress step
+ Assert.Equal(ApproachOutcome.DirectProgress, ai.LastApproach);
- ai.NextMove = 0;
- ai.MoveTo(target, 1);
- Assert.Equal(ApproachOutcome.Arrived, ai.LastApproach);
- }
- finally
- {
- bc.Delete();
- target.Delete();
- }
+ ai.NextMove = 0;
+ ai.MoveTo(target, 1);
+ Assert.Equal(ApproachOutcome.Arrived, ai.LastApproach);
}
[SkippableFact]
@@ -79,10 +75,9 @@ public class ApproachOutcomeTests
{
TileDataRequirement.SkipIfMissing();
var map = Map.Maps[1];
- var (bc, ai) = NewFollower(map, 1500, 1601);
+ var (_, ai) = NewFollower(map, 1500, 1601);
var target = NewTarget(map, 1500, 1596);
- var ring = new List
- ();
- var id = ApproachTargetTests.FirstImpassableItemId();
+ var id = TestTiles.FirstImpassableItemId();
Assert.NotEqual(0, id);
// Impassable ring around the target: unreachable.
@@ -96,35 +91,22 @@ public class ApproachOutcomeTests
}
map.GetAverageZ(x, y, out _, out var rz, out _);
- ring.Add(new Item(World.NewItem) { ItemID = id, Map = map, Location = new Point3D(x, y, (sbyte)rz) });
+ _created.Add(new Item(World.NewItem) { ItemID = id, Map = map, Location = new Point3D(x, y, (sbyte)rz) });
}
}
StepCache.Instance.Clear();
- try
+ var sawRouting = false;
+
+ for (var i = 0; i < 120; i++)
{
- var sawRouting = false;
-
- for (var i = 0; i < 120; i++)
- {
- ai.NextMove = 0;
- ai.MoveTo(target, 1);
- sawRouting |= ai.LastApproach is ApproachOutcome.Routing or ApproachOutcome.Blocked;
- }
-
- Assert.True(sawRouting, "a walled target must route or block before giving up");
- Assert.Equal(ApproachOutcome.GaveUp, ai.LastApproach);
+ ai.NextMove = 0;
+ ai.MoveTo(target, 1);
+ sawRouting |= ai.LastApproach is ApproachOutcome.Routing or ApproachOutcome.Blocked;
}
- finally
- {
- bc.Delete();
- target.Delete();
- for (var i = 0; i < ring.Count; i++)
- {
- ring[i].Delete();
- }
- }
+ Assert.True(sawRouting, "a walled target must route or block before giving up");
+ Assert.Equal(ApproachOutcome.GaveUp, ai.LastApproach);
}
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs
index f3869ffef..e330268f0 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using Server.Engines.Pathing.Cache;
using Server.Mobiles;
@@ -9,29 +10,45 @@ namespace Server.Tests.Mobiles.AI;
// pathfinder uses non-reentrant static buffers, so we share the pathfinding sequential
// collection to avoid parallel interference.
[Collection("Sequential Pathfinding Tests")]
-public class ApproachTargetTests
+public class ApproachTargetTests : IDisposable
{
- private sealed class FollowerStub : BaseCreature
+ private readonly List _created = [];
+
+ public void Dispose()
{
- // Serial ctor + DefaultMobileInit bypasses NPCSpeeds JSON (absent in tests).
- public FollowerStub(Serial serial) : base(serial) => Body = 0xC9;
+ for (var i = _created.Count - 1; i >= 0; i--)
+ {
+ _created[i].Delete();
+ }
+
+ _created.Clear();
}
- private sealed class TargetStub : Mobile
+ private (BareCreatureStub bc, BaseAI ai) NewFollower(Map map, Point3D loc)
{
- public TargetStub() => Body = 0xC9;
- }
-
- private static (FollowerStub bc, BaseAI ai) NewFollower(Map map, Point3D loc)
- {
- var bc = new FollowerStub(World.NewMobile);
+ var bc = new BareCreatureStub(World.NewMobile);
bc.DefaultMobileInit();
bc.MoveToWorld(loc, map);
+ _created.Add(bc);
BaseAI ai = new AnimalAI(bc);
ai.AITimer?.Stop(); // we drive movement manually; no background timer
return (bc, ai);
}
+ private WalkerStub NewTarget(Map map, Point3D loc)
+ {
+ var target = new WalkerStub();
+ target.MoveToWorld(loc, map);
+ _created.Add(target);
+ return target;
+ }
+
+ private void PlaceBlocker(Map map, ushort itemId, int x, int y)
+ {
+ map.GetAverageZ(x, y, out _, out var z, out _);
+ _created.Add(new Item(World.NewItem) { ItemID = itemId, Map = map, Location = new Point3D(x, y, (sbyte)z) });
+ }
+
// Drives the pet follow primitive once per tick; returns true once it reaches within
// arriveDist tiles. Uses InRange (Chebyshev) to match the game's own range semantics —
// a diagonal neighbor at Euclidean ~2.24 is "within 2" to the engine.
@@ -49,18 +66,6 @@ public class ApproachTargetTests
return false;
}
- internal static ushort FirstImpassableItemId()
- {
- for (ushort id = 1; id < TileData.MaxItemValue; id++)
- {
- if (TileData.ItemTable[id].ImpassableSurface)
- {
- return id;
- }
- }
- return 0;
- }
-
[Fact]
public void OpenTerrain_ReachesTarget_WithoutPathfinding()
{
@@ -69,8 +74,7 @@ public class ApproachTargetTests
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var (bc, ai) = NewFollower(map, new Point3D(1500, 1600, (sbyte)z));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1495, 1600, (sbyte)z), map); // 5 tiles west, open
+ var target = NewTarget(map, new Point3D(1495, 1600, (sbyte)z)); // 5 tiles west, open
StepCache.Instance.Clear();
var buildsBefore = StepCache.Instance.GetStats().BuildsTotal;
@@ -78,44 +82,40 @@ public class ApproachTargetTests
var arrived = DriveFollow(ai, bc, target, 2, 40);
var buildsAfter = StepCache.Instance.GetStats().BuildsTotal;
- bc.Delete();
- target.Delete();
Assert.True(arrived, "creature should reach an open-terrain target by stepping");
Assert.Equal(buildsBefore, buildsAfter); // greedy fast path: no chunk builds
}
- [Fact]
+ // The desk is a Britain inn static; on a blank map the detour is a straight walk.
+ [SkippableFact]
public void BritainInnDesk_PetReachesMaster()
{
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[1];
Assert.NotNull(map);
// Exact in-game repro: pet south of the L-desk, master north of it.
var (bc, ai) = NewFollower(map, new Point3D(1493, 1614, 20));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1494, 1605, 21), map);
+ var target = NewTarget(map, new Point3D(1494, 1605, 21));
StepCache.Instance.Clear();
// 200 ticks is generous for the ~17-step detour at one step per tick.
var arrived = DriveFollow(ai, bc, target, 2, 200);
- bc.Delete();
- target.Delete();
-
Assert.True(arrived, "pet must navigate around the L-desk to reach the master");
}
- [Fact]
+ [SkippableFact]
public void MoveTo_ReachesTargetAroundDesk()
{
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[1];
Assert.NotNull(map);
var (bc, ai) = NewFollower(map, new Point3D(1493, 1614, 20));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1494, 1605, 21), map);
+ var target = NewTarget(map, new Point3D(1494, 1605, 21));
StepCache.Instance.Clear();
@@ -131,9 +131,6 @@ public class ApproachTargetTests
}
}
- bc.Delete();
- target.Delete();
-
Assert.True(arrived, "MoveTo chaser must reach the target around the desk");
}
@@ -145,8 +142,7 @@ public class ApproachTargetTests
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var (bc, ai) = NewFollower(map, new Point3D(1500, 1600, (sbyte)z));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1498, 1600, (sbyte)z), map);
+ var target = NewTarget(map, new Point3D(1498, 1600, (sbyte)z));
StepCache.Instance.Clear();
@@ -170,9 +166,6 @@ public class ApproachTargetTests
}
}
- bc.Delete();
- target.Delete();
-
Assert.True(caught, "chaser must catch a target that walks away then stops");
}
@@ -185,13 +178,11 @@ public class ApproachTargetTests
map.GetAverageZ(1500, 1601, out _, out var z, out _);
var (bc, ai) = NewFollower(map, new Point3D(1500, 1601, (sbyte)z));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1500, 1596, (sbyte)z), map);
+ var target = NewTarget(map, new Point3D(1500, 1596, (sbyte)z));
// Full impassable ring around the target cell -> no path to within range 1.
- var id = FirstImpassableItemId();
+ var id = TestTiles.FirstImpassableItemId();
Assert.NotEqual(0, id);
- var ring = new List
- ();
for (var x = 1499; x <= 1501; x++)
{
for (var y = 1595; y <= 1597; y++)
@@ -200,11 +191,8 @@ public class ApproachTargetTests
{
continue; // leave the target's own cell
}
- map.GetAverageZ(x, y, out _, out var rz, out _);
- ring.Add(new Item(World.NewItem)
- {
- ItemID = id, Map = map, Location = new Point3D(x, y, (sbyte)rz)
- });
+
+ PlaceBlocker(map, id, x, y);
}
}
@@ -231,16 +219,7 @@ public class ApproachTargetTests
}
}
- var reached = bc.InRange(target, 1);
-
- bc.Delete();
- target.Delete();
- foreach (var it in ring)
- {
- it.Delete();
- }
-
- Assert.False(reached, "walled-off target must not be reached");
+ Assert.False(bc.InRange(target, 1), "walled-off target must not be reached");
Assert.True(stayedIdle, "after giving up, the creature must idle, not shuffle");
}
@@ -253,33 +232,20 @@ public class ApproachTargetTests
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var (bc, ai) = NewFollower(map, new Point3D(1500, 1600, (sbyte)z));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1500, 1595, (sbyte)z), map); // 5 tiles north
+ var target = NewTarget(map, new Point3D(1500, 1595, (sbyte)z)); // 5 tiles north
// Impassable wall on Y=1598 from X=1497..1503: blocks the straight line; ends open.
- var id = FirstImpassableItemId();
+ var id = TestTiles.FirstImpassableItemId();
Assert.NotEqual(0, id);
- var wall = new List
- ();
for (var x = 1497; x <= 1503; x++)
{
- map.GetAverageZ(x, 1598, out _, out var wz, out _);
- wall.Add(new Item(World.NewItem)
- {
- ItemID = id, Map = map, Location = new Point3D(x, 1598, (sbyte)wz)
- });
+ PlaceBlocker(map, id, x, 1598);
}
StepCache.Instance.Clear();
var arrived = DriveFollow(ai, bc, target, 2, 200);
- bc.Delete();
- target.Delete();
- foreach (var it in wall)
- {
- it.Delete();
- }
-
Assert.True(arrived, "creature must route around the wall to reach the target");
}
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/DetectHiddenTimerTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/DetectHiddenTimerTests.cs
index 383e2e103..84898b7f1 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/DetectHiddenTimerTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/DetectHiddenTimerTests.cs
@@ -1,6 +1,7 @@
using System;
using Server;
using Server.Mobiles;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Mobiles.AI;
@@ -37,9 +38,8 @@ public class DetectHiddenTimerTests
public void DetectHidden_SchedulesBoundedCooldown_AndWaitsBeforeRetry(
int intelligence, long minDelay, long maxDelay)
{
- Skip.If(!Server.Tests.TestServerInitializer.TileDataLoaded, "Requires UO client map data.");
- Core._tickCount = 0;
- Timer.Init(0);
+ Skip.If(!TestServerInitializer.TileDataLoaded, "Requires UO client map data.");
+ using var clock = new TimerWheelScope();
var creature = new DetectingCreature();
DetectingAI ai = null;
@@ -63,12 +63,8 @@ public class DetectHiddenTimerTests
Assert.InRange(ai._nextDetectHidden - Core.TickCount, minDelay, maxDelay);
Assert.True(ai.AITimer.Running);
- var deadline = ai._nextDetectHidden;
- while (Core.TickCount + 8 < deadline)
- {
- Core._tickCount += 8;
- Timer.Slice(Core.TickCount);
- }
+ // Stop one wheel step short of the cooldown: no retry may have fired yet.
+ TimerWheelScope.RunFor(ai._nextDetectHidden - TimerWheelScope.Step - Core.TickCount);
Assert.Equal(1, ai.Detections);
AdvanceUntil(() => ai.Detections > 1, 1024);
@@ -82,14 +78,9 @@ public class DetectHiddenTimerTests
}
}
- private static void AdvanceUntil(Func condition, int milliseconds)
- {
- for (var elapsed = 0; !condition() && elapsed < milliseconds; elapsed += 8)
- {
- Core._tickCount += 8;
- Timer.Slice(Core.TickCount);
- }
-
- Assert.True(condition(), "The AI timer did not reach the expected detection.");
- }
+ private static void AdvanceUntil(Func condition, int milliseconds) =>
+ Assert.True(
+ TimerWheelScope.RunUntil(condition, milliseconds),
+ "The AI timer did not reach the expected detection."
+ );
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/FamiliarAITests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/FamiliarAITests.cs
index 692ff8152..27b4ee0a0 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/FamiliarAITests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/FamiliarAITests.cs
@@ -4,8 +4,8 @@ using Server;
using Server.Items;
using Server.Mobiles;
using Server.Tests;
-using Server.Tests.Mobiles.AI;
using Xunit;
+using static Server.Tests.TimerWheelScope;
namespace UOContent.Tests.Mobiles.AI;
@@ -50,12 +50,12 @@ public class FamiliarAITests : IDisposable
private static readonly Map _map = Map.Maps[1];
private readonly List _created = new();
+ private readonly TimerWheelScope _clock;
public FamiliarAITests()
{
TileDataRequirement.SkipIfMissing();
- Core._tickCount = 0;
- Timer.Init(0);
+ _clock = new TimerWheelScope();
}
public void Dispose()
@@ -64,6 +64,8 @@ public class FamiliarAITests : IDisposable
{
_created[i].Delete();
}
+
+ _clock.Dispose();
}
private static Point3D At(int x, int y)
@@ -109,36 +111,6 @@ public class FamiliarAITests : IDisposable
return e;
}
- // 8ms lockstep keeps the wheel and Core.TickCount in sync.
- private static void RunFor(long ms)
- {
- var deadline = Core._tickCount + ms;
-
- while (Core._tickCount - deadline < 0)
- {
- Core._tickCount += 8;
- Timer.Slice(Core._tickCount);
- }
- }
-
- private static bool RunUntil(Func condition, long maxMs)
- {
- var deadline = Core._tickCount + maxMs;
-
- while (Core._tickCount - deadline < 0)
- {
- if (condition())
- {
- return true;
- }
-
- Core._tickCount += 8;
- Timer.Slice(Core._tickCount);
- }
-
- return condition();
- }
-
[SkippableFact]
public void Follows_WithoutBacktracking()
{
@@ -323,7 +295,7 @@ public class FamiliarAITests : IDisposable
var f = Familiar(0, p, 1500, 1602);
// 5x5 ring, open 3x3 interior: a landing tile exists, no route reaches it.
- var id = ApproachTargetTests.FirstImpassableItemId();
+ var id = TestTiles.FirstImpassableItemId();
Assert.NotEqual(0, id);
for (var x = 1498; x <= 1502; x++)
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs
index 785278507..f76a0e4db 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using Server;
using Server.Mobiles;
@@ -7,8 +8,20 @@ namespace UOContent.Tests.Mobiles.AI;
// Guard-following may pathfind, so this shares the pathfinding collection.
[Collection("Sequential Pathfinding Tests")]
-public class GuardFollowTests
+public class GuardFollowTests : IDisposable
{
+ private readonly List _created = [];
+
+ public void Dispose()
+ {
+ for (var i = 0; i < _created.Count; i++)
+ {
+ _created[i].Delete();
+ }
+
+ _created.Clear();
+ }
+
[Fact]
public void GuardFollow_StepsTowardMaster_AndRegistersMoveIntent()
{
@@ -16,13 +29,14 @@ public class GuardFollowTests
Assert.NotNull(map);
map.GetAverageZ(1500, 1600, out _, out var z, out _);
- var master = new PlayerMobile(World.NewMobile);
- master.DefaultMobileInit();
- master.MoveToWorld(new Point3D(1494, 1600, (sbyte)z), map);
-
- var pet = new PetTestStub();
- pet.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map); // 6 tiles east, open terrain
- pet.SetControlMaster(master);
+ // Pet 6 tiles east of the master, open terrain.
+ var (master, pet) = PetTestSetup.SpawnControlledPet(
+ new Point3D(1494, 1600, (sbyte)z),
+ new Point3D(1500, 1600, (sbyte)z),
+ map
+ );
+ _created.Add(master);
+ _created.Add(pet);
var ai = pet.AIObject;
ai.AITimer?.Stop(); // drive manually
@@ -33,21 +47,13 @@ public class GuardFollowTests
ai.NextMove = 0;
ai.Obey();
- var moved = pet.Location != start;
- var hasIntent = ai.TryGetMoveWake(out _);
- var currentSpeed = pet.CurrentSpeed;
- var currentMoveSpeed = pet.CurrentMoveSpeed;
-
- pet.Delete();
- master.Delete();
-
- Assert.True(moved, "a guarding pet beyond guard range must step toward its master");
+ Assert.True(pet.Location != start, "a guarding pet beyond guard range must step toward its master");
// Without a move intent, guard-following only steps on the think grid.
- Assert.True(hasIntent, "guard-following must register a move intent");
+ Assert.True(ai.TryGetMoveWake(out _), "guard-following must register a move intent");
// The return is paced by FollowMoveSpeed; the think clock stays on the active value
// and the per-step speed flip must not undo either.
- Assert.Equal(0.2, currentSpeed);
- Assert.Equal(0.1, currentMoveSpeed);
+ Assert.Equal(0.2, pet.CurrentSpeed);
+ Assert.Equal(pet.FollowMoveSpeed, pet.CurrentMoveSpeed);
}
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs
index ec11a3ba5..ee3bfe7cd 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs
@@ -11,10 +11,24 @@ public class GuardFriendlyFireTests : IDisposable
{
private readonly List _created = [];
+ // AI_Mage stands in for a shipped caster: MageAI's combat loop has its own reacquire path.
+ private sealed class MagePet : BaseCreature
+ {
+ public MagePet() : base(AIType.AI_Mage, FightMode.Closest, 10, 1) => Body = 0xC8;
+
+ public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
+ {
+ activeSpeed = 0.2;
+ passiveSpeed = 0.4;
+ }
+ }
+
private (PlayerMobile owner, PetTestStub pet) Spawn()
{
Skip.If(!Server.Tests.TestServerInitializer.TileDataLoaded, "Requires UO client map data.");
var z = Map.Felucca.GetAverageZ(1500, 1600);
+ // The constructed PlayerMobile, not PetTestSetup's deserialization-shaped one: the harmful
+ // checks read PermaFlags, which only the full constructor allocates.
var owner = new PlayerMobile();
owner.MoveToWorld(new Point3D(1500, 1600, z), Map.Felucca);
_created.Add(owner);
@@ -192,28 +206,28 @@ public class GuardFriendlyFireTests : IDisposable
}
[SkippableFact]
- public void ActualWhiteWyrmDoesNotTurnOnOwnerWhenEnemyDisappears()
+ public void GuardingMageDoesNotTurnOnOwnerWhenEnemyDisappears()
{
var (owner, _) = Spawn();
- var wyrm = new WhiteWyrm();
- _created.Add(wyrm);
- wyrm.MoveToWorld(new Point3D(1498, 1600, owner.Z), owner.Map);
- wyrm.SetControlMaster(owner);
- wyrm.IssueOrder(OrderType.Guard, owner);
- wyrm.AIObject.AITimer.Stop();
+ var mage = new MagePet();
+ _created.Add(mage);
+ mage.MoveToWorld(new Point3D(1498, 1600, owner.Z), owner.Map);
+ mage.SetControlMaster(owner);
+ mage.IssueOrder(OrderType.Guard, owner);
+ mage.AIObject.AITimer.Stop();
var enemy = new PetTestStub();
_created.Add(enemy);
enemy.MoveToWorld(new Point3D(1497, 1600, owner.Z), owner.Map);
enemy.AIObject.AITimer.Stop();
- wyrm.Combatant = enemy;
+ mage.Combatant = enemy;
enemy.Hidden = true;
- wyrm.NextReacquireTime = Core.TickCount - 1;
- Assert.IsType(wyrm.AIObject);
- wyrm.AIObject.DoActionCombat();
- Assert.NotSame(owner, wyrm.Combatant);
- Assert.Null(wyrm.FocusMob);
- wyrm.AIObject.DoOrderGuard();
- Assert.Null(wyrm.Combatant);
+ mage.NextReacquireTime = Core.TickCount - 1;
+ Assert.IsType(mage.AIObject);
+ mage.AIObject.DoActionCombat();
+ Assert.NotSame(owner, mage.Combatant);
+ Assert.Null(mage.FocusMob);
+ mage.AIObject.DoOrderGuard();
+ Assert.Null(mage.Combatant);
}
public void Dispose()
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs
index 7d572786a..356994020 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Server;
using Server.Mobiles;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Mobiles.AI;
@@ -14,11 +15,6 @@ public class GuardOrderTests : IDisposable
{
private readonly List _created = new();
- private sealed class AggressorStub : Mobile
- {
- public AggressorStub() => Body = 0xC9;
- }
-
public void Dispose()
{
foreach (var m in _created)
@@ -35,14 +31,12 @@ public class GuardOrderTests : IDisposable
Assert.NotNull(map);
map.GetAverageZ(1500, 1600, out _, out z, out _);
- var master = new PlayerMobile(World.NewMobile);
- master.DefaultMobileInit();
- master.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map);
+ var (master, pet) = PetTestSetup.SpawnControlledPet(
+ new Point3D(1500, 1600, (sbyte)z),
+ new Point3D(1499, 1600, (sbyte)z),
+ map
+ );
_created.Add(master);
-
- var pet = new PetTestStub();
- pet.MoveToWorld(new Point3D(1499, 1600, (sbyte)z), map);
- pet.SetControlMaster(master);
_created.Add(pet);
pet.AIObject.AITimer?.Stop(); // drive manually
@@ -52,9 +46,9 @@ public class GuardOrderTests : IDisposable
return (master, pet);
}
- private AggressorStub SpawnAggressor(PetTestStub pet, Point3D loc, Mobile attacking)
+ private WalkerStub SpawnAggressor(PetTestStub pet, Point3D loc, Mobile attacking)
{
- var aggr = new AggressorStub();
+ var aggr = new WalkerStub();
aggr.MoveToWorld(loc, pet.Map);
_created.Add(aggr);
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/HerdingTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/HerdingTests.cs
index 357a39979..6c88649fa 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/HerdingTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/HerdingTests.cs
@@ -10,11 +10,6 @@ namespace UOContent.Tests.Mobiles.AI;
[Collection("Sequential Pathfinding Tests")]
public class HerdingTests
{
- private sealed class Stub : BaseCreature
- {
- public Stub(Serial serial) : base(serial) => Body = 0xC9;
- }
-
[SkippableFact]
public void HerdedCreature_ReachesTheTile_AndClearsTargetLocation()
{
@@ -22,7 +17,7 @@ public class HerdingTests
var map = Map.Maps[1];
map.GetAverageZ(1500, 1600, out _, out var z, out _);
- var bc = new Stub(World.NewMobile);
+ var bc = new BareCreatureStub(World.NewMobile);
bc.DefaultMobileInit();
bc.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map);
BaseAI ai = new AnimalAI(bc);
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs
index 2789cd735..288572eb7 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs
@@ -210,10 +210,10 @@ public class PetOrderTests : IDisposable
Assert.Equal(post, pet.Home); // anchor still the original post, not the corpse
}
- // NOTE: the test fixture does not load tile data, so Mobile.Move is blocked and Location
- // never changes here. DoMoveImpl still sets Mobile.Direction before the (blocked) move,
- // so an *attempted* wander is observable via Direction. The subjective wander cadence is
- // covered by manual QA; these tests verify the gate/frozen wiring deterministically.
+ // A step may be blocked by whatever terrain (1000, 1000) holds, but DoMoveImpl sets
+ // Mobile.Direction before attempting the move, so an *attempted* wander is observable via
+ // Direction either way. The subjective wander cadence is covered by manual QA; these tests
+ // verify the gate/frozen wiring deterministically.
[Fact]
public void IdlePet_DoesNotAttemptToMove_WhileResting()
{
@@ -224,12 +224,18 @@ public class PetOrderTests : IDisposable
pet.ForceIdle = true; // CheckIdle() reports resting -> idle wander must be skipped
pet.Direction = Direction.North;
+ var start = pet.Location;
+ var ai = pet.AIObject;
for (var i = 0; i < 40; i++)
{
- pet.AIObject.DoOrderNone();
+ // Reopen the move clock every pass, so CheckIdle is the only gate left standing.
+ ai.NextMove = Core.TickCount;
+ Assert.True(ai.CanMoveNow(out _));
+ ai.DoOrderNone();
}
Assert.Equal(Direction.North, pet.Direction); // gated by CheckIdle -> never attempts a step
+ Assert.Equal(start, pet.Location);
}
[Fact]
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.cs
index 1064fb317..793fd3d6f 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.cs
@@ -2,7 +2,9 @@ using System;
using System.Collections.Generic;
using Server;
using Server.Mobiles;
+using Server.Tests;
using Xunit;
+using static Server.Tests.TimerWheelScope;
namespace UOContent.Tests.Mobiles.AI;
@@ -11,6 +13,7 @@ namespace UOContent.Tests.Mobiles.AI;
public class PetPacingTests : IDisposable
{
private readonly List _created = new();
+ private TimerWheelScope _clock;
private (PlayerMobile master, PetTestStub pet) Spawn(Point3D masterLoc, Point3D petLoc)
{
@@ -28,6 +31,7 @@ public class PetPacingTests : IDisposable
}
_created.Clear();
+ _clock?.Dispose();
}
// Movement orders run active, resting orders run passive; the move clock follows.
@@ -64,8 +68,8 @@ public class PetPacingTests : IDisposable
pet.ControlTarget = master;
pet.ControlOrder = OrderType.Follow; // fixture era is EJ
- Assert.Equal(0.2, pet.CurrentSpeed); // active think, not the follow pace
- Assert.Equal(0.1, pet.CurrentMoveSpeed); // capped at the follow pace
+ Assert.Equal(0.2, pet.CurrentSpeed); // active think, not the follow pace
+ Assert.Equal(pet.FollowMoveSpeed, pet.CurrentMoveSpeed); // capped at the follow pace
}
// A creature configured faster than the follow pace keeps its own.
@@ -73,12 +77,13 @@ public class PetPacingTests : IDisposable
public void FollowMaster_KeepsAFasterConfiguredPace()
{
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
- pet.SetMoveSpeed(0.05, 0.9);
+ var faster = pet.FollowMoveSpeed / 2;
+ pet.SetMoveSpeed(faster, 0.9);
pet.ControlTarget = master;
pet.ControlOrder = OrderType.Follow;
- Assert.Equal(0.05, pet.CurrentMoveSpeed);
+ Assert.Equal(faster, pet.CurrentMoveSpeed);
}
// The move-clock override survives the order: it is capped while following, not overwritten.
@@ -145,7 +150,7 @@ public class PetPacingTests : IDisposable
pet.ControlOrder = OrderType.Guard;
Assert.Equal(0.2, pet.CurrentSpeed);
- Assert.Equal(0.1, pet.CurrentMoveSpeed);
+ Assert.Equal(pet.FollowMoveSpeed, pet.CurrentMoveSpeed);
}
// Obeying the follow order must not write the pace into either clock.
@@ -161,7 +166,7 @@ public class PetPacingTests : IDisposable
pet.AIObject.Obey();
Assert.Equal(0.2, pet.CurrentSpeed);
- Assert.Equal(0.1, pet.CurrentMoveSpeed);
+ Assert.Equal(pet.FollowMoveSpeed, pet.CurrentMoveSpeed);
}
// At the master's side a guarding pet stays active: no stale-warmode passive, no sprint.
@@ -208,41 +213,10 @@ public class PetPacingTests : IDisposable
return (master, pet);
}
- // Advances time in 8ms lockstep so the wheel and Core.TickCount stay in sync.
- private static void RunFor(long ms)
- {
- var deadline = Core._tickCount + ms;
-
- while (Core._tickCount < deadline)
- {
- Core._tickCount += 8;
- Timer.Slice(Core._tickCount);
- }
- }
-
- private static bool RunUntil(Func condition, long maxMs)
- {
- var deadline = Core._tickCount + maxMs;
-
- while (Core._tickCount < deadline)
- {
- if (condition())
- {
- return true;
- }
-
- Core._tickCount += 8;
- Timer.Slice(Core._tickCount);
- }
-
- return condition();
- }
-
// Runs past the spawn stagger; returns right after a think with the next 0.4s away.
private ThinkProbe SettledProbe(out PlayerMobile master)
{
- Core._tickCount = 0;
- Timer.Init(0);
+ _clock = new TimerWheelScope();
var (m, pet) = SpawnProbe();
master = m;
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs
index 41b52a189..dec588d49 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs
@@ -35,16 +35,10 @@ public class PetRetaliationTests : IDisposable
private (PlayerMobile master, T pet) Spawn() where T : BaseCreature, new()
{
- var master = new PlayerMobile(World.NewMobile);
- master.DefaultMobileInit();
- master.MoveToWorld(new Point3D(1000, 1000, 0), Map.Felucca);
+ var (master, pet) = PetTestSetup.SpawnControlledPet(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
_created.Add(master);
-
- var pet = new T();
- pet.MoveToWorld(new Point3D(1001, 1000, 0), Map.Felucca);
- pet.SetControlMaster(master);
- pet.AIObject.AITimer?.Stop();
_created.Add(pet);
+ pet.AIObject.AITimer?.Stop();
return (master, pet);
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetTestStub.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetTestStub.cs
index d15228590..7779384b6 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetTestStub.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetTestStub.cs
@@ -49,17 +49,21 @@ public class PetTestStub : BaseCreature
public static class PetTestSetup
{
- // Places a player master and a controlled pet on Felucca and returns both.
+ // Places a player master and a controlled pet (on Felucca unless a map is given) and returns both.
public static (PlayerMobile master, PetTestStub pet) SpawnControlledPet(
- Point3D masterLoc, Point3D petLoc)
+ Point3D masterLoc, Point3D petLoc, Map map = null) =>
+ SpawnControlledPet(masterLoc, petLoc, map);
+
+ public static (PlayerMobile master, T pet) SpawnControlledPet(
+ Point3D masterLoc, Point3D petLoc, Map map = null) where T : BaseCreature, new()
{
- var map = Map.Felucca;
+ map ??= Map.Felucca;
var master = new PlayerMobile(World.NewMobile);
master.DefaultMobileInit();
master.MoveToWorld(masterLoc, map);
- var pet = new PetTestStub();
+ var pet = new T();
pet.MoveToWorld(petLoc, map);
pet.SetControlMaster(master); // sets Controlled, Home=Zero, ControlOrder=Come
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs
index ba106218c..5d92fca85 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Server;
using Server.Items;
using Server.Mobiles;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Mobiles;
@@ -15,60 +16,14 @@ namespace UOContent.Tests.Mobiles;
[Collection("Sequential UOContent Tests")]
public class BaseCreatureSerializationTests : IDisposable
{
- private readonly List _created = new();
- private readonly List
- _createdItems = new();
+ private readonly MobileTestHarness _harness = new();
- public void Dispose()
- {
- for (var i = 0; i < _created.Count; i++)
- {
- _created[i].Delete();
- }
+ public void Dispose() => _harness.Dispose();
- for (var i = 0; i < _createdItems.Count; i++)
- {
- _createdItems[i].Delete();
- }
- }
-
- private class CreatureStub : BaseCreature
- {
- public CreatureStub() : base(AIType.AI_Melee) => Body = 0xC9;
-
- public CreatureStub(Serial serial) : base(serial) => Body = 0xC9;
-
- public DateTime SummonEndValue => SummonEnd;
-
- // Stands in for the npc-speeds table (unconfigured in the test fixture).
- public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
- {
- activeSpeed = 0.3;
- passiveSpeed = 0.6;
- }
-
- public override void GetMoveSpeeds(out double activeMoveSpeed, out double passiveMoveSpeed)
- {
- activeMoveSpeed = 0.6;
- passiveMoveSpeed = 1.2;
- }
- }
-
- private CreatureStub NewCreature()
- {
- var bc = new CreatureStub();
- _created.Add(bc);
- return bc;
- }
+ private TestCreature NewCreature() => _harness.NewCreature();
// ReadEntity resolves references through the world table, so the master must be registered.
- private PlayerMobile NewMaster()
- {
- var master = new PlayerMobile(World.NewMobile);
- master.DefaultMobileInit();
- World.AddEntity(master);
- _created.Add(master);
- return master;
- }
+ private PlayerMobile NewMaster() => _harness.NewPlayer();
private static byte[] Snapshot(Mobile m)
{
@@ -80,10 +35,10 @@ public class BaseCreatureSerializationTests : IDisposable
return buffer;
}
- private CreatureStub Load(byte[] buffer)
+ private TestCreature Load(byte[] buffer)
{
- var copy = new CreatureStub(World.NewMobile);
- _created.Add(copy);
+ var copy = new TestCreature(World.NewMobile);
+ _harness.Track(copy);
var reader = new BufferReader(buffer);
copy.Deserialize(reader);
@@ -219,7 +174,7 @@ public class BaseCreatureSerializationTests : IDisposable
var bc = NewCreature();
var friend = NewMaster();
var wayPoint = new WayPoint();
- _createdItems.Add(wayPoint);
+ _harness.TrackItem(wayPoint);
bc.AddPetFriend(friend);
bc.CurrentWayPoint = wayPoint;
@@ -245,49 +200,20 @@ public class BaseCreatureSerializationTests : IDisposable
Assert.InRange(copy.DeleteTimeLeft, TimeSpan.FromDays(3.0) - TimeSpan.FromSeconds(5), TimeSpan.FromDays(3.0));
}
- private sealed class VendorStub : BaseVendor
- {
- private static readonly List _sbInfos = [];
-
- public VendorStub() : base("the stub")
- {
- }
-
- public VendorStub(Serial serial) : base(serial)
- {
- }
-
- protected override List SBInfos => _sbInfos;
-
- public override void InitSBInfo()
- {
- }
-
- public override void InitOutfit()
- {
- }
-
- public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
- {
- activeSpeed = 0.3;
- passiveSpeed = 0.6;
- }
- }
-
// BaseVendor is generated on top of the generated BaseCreature; the chain must write
// and read both sections in order with exact consumption.
[Fact]
public void GeneratedVendorChain_RoundTrips()
{
- var vendor = new VendorStub();
- _created.Add(vendor);
+ var vendor = new TestVendor();
+ _harness.Track(vendor);
vendor.Home = new Point3D(1500, 1600, 0);
vendor.RangeHome = 2;
var buffer = Snapshot(vendor);
- var copy = new VendorStub(World.NewMobile);
- _created.Add(copy);
+ var copy = new TestVendor(World.NewMobile);
+ _harness.Track(copy);
var reader = new BufferReader(buffer);
copy.Deserialize(reader);
@@ -378,7 +304,7 @@ public class BaseCreatureSerializationTests : IDisposable
writer.Write(0.0); // passive move speed (v22)
}
- private CreatureStub LoadLegacyV22(
+ private TestCreature LoadLegacyV22(
bool controlled = false,
Mobile controlMaster = null,
bool summoned = false,
@@ -390,7 +316,7 @@ public class BaseCreatureSerializationTests : IDisposable
// Mobile donor produces a byte-authentic one.
var donor = new MobileStub();
donor.DefaultMobileInit();
- _created.Add(donor);
+ _harness.Track(donor);
var writer = new BufferWriter(true);
donor.Serialize(writer);
@@ -399,8 +325,8 @@ public class BaseCreatureSerializationTests : IDisposable
var buffer = new byte[writer.Position];
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
- var copy = new CreatureStub(World.NewMobile);
- _created.Add(copy);
+ var copy = new TestCreature(World.NewMobile);
+ _harness.Track(copy);
var reader = new BufferReader(buffer);
copy.Deserialize(reader);
@@ -499,7 +425,7 @@ public class BaseCreatureSerializationTests : IDisposable
var donor = new MobileStub();
donor.DefaultMobileInit();
- _created.Add(donor);
+ _harness.Track(donor);
var writer = new BufferWriter(true);
donor.Serialize(writer);
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/DeletedMasterTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/DeletedMasterTests.cs
index f01e5c7bd..be314b575 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/DeletedMasterTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/DeletedMasterTests.cs
@@ -1,8 +1,8 @@
using System;
-using System.Collections.Generic;
using Server;
using Server.Mobiles;
using Server.Spells.Necromancy;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Mobiles;
@@ -11,40 +11,13 @@ namespace UOContent.Tests.Mobiles;
[Collection("Sequential UOContent Tests")]
public class DeletedMasterTests : IDisposable
{
- private readonly List _created = [];
+ private readonly MobileTestHarness _harness = new();
- public void Dispose()
- {
- for (var i = 0; i < _created.Count; i++)
- {
- _created[i].Delete();
- }
- }
+ public void Dispose() => _harness.Dispose();
- private sealed class Stub : BaseCreature
- {
- public Stub() : base(AIType.AI_Melee) => Body = 0xC9;
+ private TestCreature NewCreature() => _harness.NewCreature();
- public Stub(Serial serial) : base(serial)
- {
- }
- }
-
- private Stub NewCreature()
- {
- var bc = new Stub();
- _created.Add(bc);
- return bc;
- }
-
- private PlayerMobile NewPlayer()
- {
- var pm = new PlayerMobile(World.NewMobile);
- pm.DefaultMobileInit();
- World.AddEntity(pm);
- _created.Add(pm);
- return pm;
- }
+ private PlayerMobile NewPlayer() => _harness.NewPlayer();
[Fact]
public void DeletingAPlayer_ReleasesAnUnbondedPet()
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs
index 20578ecfc..bb027a951 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs
@@ -1,7 +1,7 @@
using System;
-using System.Collections.Generic;
using Server;
using Server.Mobiles;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Mobiles;
@@ -10,40 +10,13 @@ namespace UOContent.Tests.Mobiles;
[Collection("Sequential UOContent Tests")]
public class MasterViewsTests : IDisposable
{
- private readonly List _created = [];
+ private readonly MobileTestHarness _harness = new();
- public void Dispose()
- {
- for (var i = 0; i < _created.Count; i++)
- {
- _created[i].Delete();
- }
- }
+ public void Dispose() => _harness.Dispose();
- private sealed class Stub : BaseCreature
- {
- public Stub() : base(AIType.AI_Melee) => Body = 0xC9;
+ private TestCreature NewCreature() => _harness.NewCreature();
- public Stub(Serial serial) : base(serial)
- {
- }
- }
-
- private Stub NewCreature()
- {
- var bc = new Stub();
- _created.Add(bc);
- return bc;
- }
-
- private PlayerMobile NewPlayer()
- {
- var pm = new PlayerMobile(World.NewMobile);
- pm.DefaultMobileInit();
- World.AddEntity(pm);
- _created.Add(pm);
- return pm;
- }
+ private PlayerMobile NewPlayer() => _harness.NewPlayer();
public enum Shape
{
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/VendorBuybackTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/VendorBuybackTests.cs
index f61ca6229..fac93cb54 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/VendorBuybackTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/VendorBuybackTests.cs
@@ -9,15 +9,9 @@ namespace Server.Tests.Mobiles;
[Collection("Sequential UOContent Tests")]
public class VendorBuybackTests : IDisposable
{
- private readonly List _created = [];
+ private readonly MobileTestHarness _harness = new();
- public void Dispose()
- {
- for (var i = 0; i < _created.Count; i++)
- {
- _created[i].Delete();
- }
- }
+ public void Dispose() => _harness.Dispose();
private sealed class BandageSB : SBInfo
{
@@ -30,34 +24,9 @@ public class VendorBuybackTests : IDisposable
public override List BuyInfo { get; } = [];
}
- private sealed class VendorStub : BaseVendor
+ private sealed class BandageVendor : TestVendor
{
- private readonly List _sbInfos = [];
-
- public VendorStub() : base("the stub")
- {
- }
-
- public VendorStub(Serial serial) : base(serial)
- {
- }
-
- protected override List SBInfos => _sbInfos;
-
- public override void InitSBInfo()
- {
- _sbInfos.Add(new BandageSB());
- }
-
- public override void InitOutfit()
- {
- }
-
- public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
- {
- activeSpeed = 0.3;
- passiveSpeed = 0.6;
- }
+ protected override void AddSBInfos(List sbInfos) => sbInfos.Add(new BandageSB());
}
// Same sell price as BandageSB, but a buy list at the 250-entry limit so buyback capacity is 0.
@@ -81,55 +50,36 @@ public class VendorBuybackTests : IDisposable
public override List BuyInfo => _buyInfo;
}
- private sealed class FullStockVendorStub : BaseVendor
+ private sealed class FullStockVendor : TestVendor
{
- private readonly List _sbInfos = [];
-
- public FullStockVendorStub() : base("the stub")
- {
- }
-
- protected override List SBInfos => _sbInfos;
-
- public override void InitSBInfo()
- {
- _sbInfos.Add(new FullStockSB());
- }
-
- public override void InitOutfit()
- {
- }
-
- public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
- {
- activeSpeed = 0.3;
- passiveSpeed = 0.6;
- }
+ protected override void AddSBInfos(List sbInfos) => sbInfos.Add(new FullStockSB());
}
- private VendorStub NewVendor()
- {
- var vendor = new VendorStub();
- _created.Add(vendor);
- return vendor;
- }
+ private BandageVendor NewVendor() => _harness.Track(new BandageVendor());
- private FullStockVendorStub NewFullStockVendor()
- {
- var vendor = new FullStockVendorStub();
- _created.Add(vendor);
- return vendor;
- }
+ private FullStockVendor NewFullStockVendor() => _harness.Track(new FullStockVendor());
private Mobile NewSeller()
{
- var seller = new Mobile(World.NewMobile);
+ var seller = _harness.Track(new Mobile(World.NewMobile));
seller.DefaultMobileInit();
seller.AddItem(new Backpack());
- _created.Add(seller);
return seller;
}
+ // A fresh seller offers a stack of 10 bandages and sells of them.
+ private Bandage SellBandages(BaseVendor vendor, int amount, out Mobile seller)
+ {
+ seller = NewSeller();
+ var bandages = new Bandage(10);
+ seller.Backpack.DropItem(bandages);
+
+ Assert.True(vendor.OnSellItems(seller, [new SellItemResponse(bandages, amount)]));
+ return bandages;
+ }
+
+ private Bandage SellBandages(BaseVendor vendor, int amount) => SellBandages(vendor, amount, out _);
+
[Theory]
[InlineData(0.25, 1.0)] // restocked 15m ago: next boundary is the regular one
[InlineData(5.0, 6.0)] // exactly on a boundary: strictly after now
@@ -160,11 +110,7 @@ public class VendorBuybackTests : IDisposable
public void PartialStackSale_SplitGoesToBuyback_SellerKeepsRemainder()
{
var vendor = NewVendor();
- var seller = NewSeller();
- var bandages = new Bandage(10);
- seller.Backpack.DropItem(bandages);
-
- Assert.True(vendor.OnSellItems(seller, [new SellItemResponse(bandages, 4)]));
+ var bandages = SellBandages(vendor, 4, out var seller);
Assert.Equal(6, bandages.Amount);
Assert.Same(seller.Backpack, bandages.Parent);
@@ -181,10 +127,7 @@ public class VendorBuybackTests : IDisposable
public void Restock_PurgesBuyback()
{
var vendor = NewVendor();
- var seller = NewSeller();
- var bandages = new Bandage(10);
- seller.Backpack.DropItem(bandages);
- vendor.OnSellItems(seller, [new SellItemResponse(bandages, 10)]);
+ var bandages = SellBandages(vendor, 10);
Assert.Single(vendor.BuyPack.Items);
vendor.Restock();
@@ -197,20 +140,14 @@ public class VendorBuybackTests : IDisposable
public void Restock_ClearsPurgeSchedule_AndASubsequentSaleRearmsIt()
{
var vendor = NewVendor();
- var seller = NewSeller();
- var bandages = new Bandage(10);
- seller.Backpack.DropItem(bandages);
- vendor.OnSellItems(seller, [new SellItemResponse(bandages, 10)]);
+ SellBandages(vendor, 10);
Assert.True(vendor.BuybackPurgeScheduled);
vendor.Restock();
Assert.False(vendor.BuybackPurgeScheduled);
- var seller2 = NewSeller();
- var moreBandages = new Bandage(10);
- seller2.Backpack.DropItem(moreBandages);
- vendor.OnSellItems(seller2, [new SellItemResponse(moreBandages, 10)]);
+ SellBandages(vendor, 10);
Assert.True(vendor.BuybackPurgeScheduled);
}
@@ -219,10 +156,7 @@ public class VendorBuybackTests : IDisposable
public void Delete_CancelsBuybackPurgeTimer()
{
var vendor = NewVendor();
- var seller = NewSeller();
- var bandages = new Bandage(10);
- seller.Backpack.DropItem(bandages);
- vendor.OnSellItems(seller, [new SellItemResponse(bandages, 10)]);
+ SellBandages(vendor, 10);
Assert.True(vendor.BuybackPurgeScheduled);
vendor.Delete();
@@ -239,7 +173,8 @@ public class VendorBuybackTests : IDisposable
var legacy = new Backpack { Layer = Layer.ShopBuy, Movable = false, Visible = false };
vendor.AddItem(legacy);
- var children = new Item[20_000];
+ // Several children, so a delete loop that skips entries as the list shrinks would show.
+ var children = new Item[3];
for (var i = 0; i < children.Length; i++)
{
children[i] = new Item(0x1234);
@@ -261,11 +196,7 @@ public class VendorBuybackTests : IDisposable
public void SaleWithNoBuybackCapacity_ConsumesItem_AndStillPays()
{
var vendor = NewFullStockVendor();
- var seller = NewSeller();
- var bandages = new Bandage(10);
- seller.Backpack.DropItem(bandages);
-
- Assert.True(vendor.OnSellItems(seller, [new SellItemResponse(bandages, 10)]));
+ var bandages = SellBandages(vendor, 10, out var seller);
Assert.True(bandages.Deleted);
Assert.Empty(vendor.BuyPack.Items);
diff --git a/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs b/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs
index b981d9f86..d603a0c12 100644
--- a/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs
+++ b/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs
@@ -216,7 +216,8 @@ public sealed class DesignStateDetailed : Packet
if (deflatedLength == 0)
{
- Console.WriteLine("Compression error");
+ // An oracle that silently emits a truncated plane would make the comparison meaningless.
+ throw new InvalidOperationException("Compression error");
}
Write((byte)(0x20 | i));
@@ -248,7 +249,8 @@ public sealed class DesignStateDetailed : Packet
if (deflatedLength == 0)
{
- Console.WriteLine("Compression error");
+ // An oracle that silently emits a truncated plane would make the comparison meaningless.
+ throw new InvalidOperationException("Compression error");
}
Write((byte)(9 + i));
diff --git a/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs b/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs
index b3a6d291f..2faa8eba5 100644
--- a/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs
@@ -13,7 +13,9 @@
* along with this program. If not, see . *
*************************************************************************/
+using System;
using System.Net;
+using System.Reflection;
using System.Text;
using Server.Network;
using Server.Network.Bans;
@@ -21,10 +23,18 @@ using Xunit;
namespace Server.Tests.Network.Exemptions;
-// Addresses come from TEST-NET-1 (192.0.2.0/24) to stay clear of the other ban test classes if xUnit runs
-// them concurrently.
-public class BanExemptionsTests
+// ManualAllowlist is process-wide and BlocklistFilter reads it, so this runs in the sequential collection
+// and puts the original snapshot back. Addresses come from TEST-NET-1 (192.0.2.0/24).
+[Collection("Sequential UOContent Tests")]
+public class BanExemptionsTests : IDisposable
{
+ private static readonly FieldInfo _snapshotField =
+ typeof(ManualAllowlist).GetField("_snapshot", BindingFlags.Static | BindingFlags.NonPublic);
+
+ private readonly object _savedSnapshot = _snapshotField.GetValue(null);
+
+ public void Dispose() => ManualAllowlist.LoadForTesting((BlocklistSnapshot)_savedSnapshot);
+
private static readonly IPAddress _listed = IPAddress.Parse("192.0.2.10");
private static readonly IPAddress _unlisted = IPAddress.Parse("192.0.2.11");
diff --git a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFileTests.cs b/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFileTests.cs
index 1e1c0fedf..991e49451 100644
--- a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFileTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFileTests.cs
@@ -21,14 +21,13 @@ using Xunit;
namespace Server.Tests.Network.Bans.Blocklist;
-public class BlocklistFileTests
+public class BlocklistFileTests : IDisposable
{
- private static string WriteTemp(string content)
- {
- var p = Path.Combine(Path.GetTempPath(), "bl-" + Guid.NewGuid().ToString("N") + ".txt");
- File.WriteAllText(p, content);
- return p;
- }
+ private readonly TempDirectory _temp = new("bl");
+
+ public void Dispose() => _temp.Dispose();
+
+ private string WriteTemp(string content) => _temp.WriteFile("blocklist.txt", content);
[Fact]
public void Reads_header_generated_and_count()
@@ -38,13 +37,12 @@ public class BlocklistFileTests
Assert.True(h.Present);
Assert.Equal("2026-07-21T09:24:23Z", h.Generated);
Assert.Equal(2, h.Count);
- File.Delete(p);
}
[Fact]
public void Missing_file_reports_absent_and_loads_empty()
{
- var p = Path.Combine(Path.GetTempPath(), "does-not-exist-" + Guid.NewGuid().ToString("N"));
+ var p = _temp.Combine("does-not-exist.txt");
Assert.False(BlocklistFile.TryReadHeader(p, out var h));
Assert.False(h.Present);
var snap = BlocklistFile.Load(p, out _, out _);
@@ -70,6 +68,5 @@ public class BlocklistFileTests
Assert.Equal(0, skipped);
Assert.True(snap.IsBanned(IPAddress.Parse("2.181.183.77")));
Assert.True(snap.IsBanned(IPAddress.Parse("223.169.4.9")));
- File.Delete(p);
}
}
diff --git a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFilterTests.cs b/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFilterTests.cs
index 4f3ee5ad1..6e630ba49 100644
--- a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFilterTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFilterTests.cs
@@ -20,8 +20,10 @@ using Xunit;
namespace Server.Tests.Network.Bans.Blocklist;
-// No [Collection] and no static reset hook: the filter is an instance, so each test owns its own
-// snapshot and promote-guard. That is the point of it no longer being a static class.
+// The filter is an instance, so each test owns its own snapshot and promote-guard. Evaluate still
+// consults the process-wide ManualAllowlist and LoginAllowlist, which other tests rewrite, so this
+// runs in the sequential collection.
+[Collection("Sequential UOContent Tests")]
public class BlocklistFilterTests
{
private static BlocklistFilter WithList(string list, bool reportHits = true, long suppressionMs = 5000)
diff --git a/Projects/UOContent.Tests/Tests/Network/Bans/CrowdSecReporterTests.cs b/Projects/UOContent.Tests/Tests/Network/Bans/CrowdSecReporterTests.cs
index 6ad850f34..0a6d956d6 100644
--- a/Projects/UOContent.Tests/Tests/Network/Bans/CrowdSecReporterTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Bans/CrowdSecReporterTests.cs
@@ -218,14 +218,18 @@ public class CrowdSecReporterTests
///
/// The drain task must track the loop's lifetime, not just its first await — a ValueTask-returning
- /// drain loop passed to Task.Run yields a Task<ValueTask> that completes immediately, which makes
- /// Stop()'s drain-exited handshake a no-op. With an empty queue the loop parks on WaitToReadAsync,
- /// so a correctly unwrapped task cannot win this race; a slow pool only under-detects.
+ /// drain loop passed to Task.Run yields a Task<ValueTask> that completes at the first await
+ /// that suspends, which makes Stop()'s drain-exited handshake a no-op. The flush delay always
+ /// suspends before the first post, so once the client sees a post a wrapped task is already done.
///
[Fact]
public async Task Start_DrainTaskSpansLoopLifetime_NotJustTheFirstAwait()
{
- var reporter = new CrowdSecReporter(new NullAlertClient(), Settings());
+ var settings = Settings();
+ settings.FlushInterval = TimeSpan.FromMilliseconds(1);
+
+ var client = new SignalingAlertClient();
+ var reporter = new CrowdSecReporter(client, settings);
using var cts = new CancellationTokenSource();
reporter.Start(cts.Token);
@@ -233,14 +237,32 @@ public class CrowdSecReporterTests
var drain = reporter.DrainTaskForTesting;
Assert.NotNull(drain);
- var first = await Task.WhenAny(drain, Task.Delay(TimeSpan.FromMilliseconds(500)));
- Assert.False(ReferenceEquals(first, drain), "drain task completed while the loop was still running");
+ reporter.Report(IPAddress.Parse("9.9.9.9"), TimeSpan.FromHours(1), "rate-limit");
+ await client.Posted.Task.WaitAsync(TimeSpan.FromSeconds(10));
+
+ Assert.False(drain.IsCompleted, "drain task completed while the loop was still running");
reporter.Stop();
Assert.True(drain.IsCompleted, "Stop() returned before the drain loop exited");
}
+ private sealed class SignalingAlertClient : ICrowdSecAlertClient
+ {
+ public TaskCompletionSource Posted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ public ValueTask PostAlertsAsync(IReadOnlyList alerts, CancellationToken token)
+ {
+ Posted.TrySetResult();
+ return ValueTask.CompletedTask;
+ }
+
+ public ValueTask DeleteDecisionsAsync(string origin, IPAddress ip, CancellationToken token) =>
+ ValueTask.CompletedTask;
+
+ public void Dispose() { }
+ }
+
private sealed class NullAlertClient : ICrowdSecAlertClient
{
public ValueTask PostAlertsAsync(IReadOnlyList alerts, CancellationToken token) =>
diff --git a/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallPersistenceTests.cs b/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallPersistenceTests.cs
index 01691e223..c5617c0d3 100644
--- a/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallPersistenceTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallPersistenceTests.cs
@@ -21,8 +21,11 @@ using Xunit;
namespace Server.Tests.Network.Firewall;
[Collection("Sequential UOContent Tests")]
-public class FirewallPersistenceTests
+public class FirewallPersistenceTests : IDisposable
{
+ // The test host never configures the firewall, so empty is the state every other test expects.
+ public void Dispose() => Server.Network.Firewall.ResetForTesting();
+
[Fact]
public void ToSettings_RoundTrips_PermanentAndTtl()
{
diff --git a/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallTests.cs b/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallTests.cs
index 26e289f0c..7dba7508c 100644
--- a/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallTests.cs
@@ -21,8 +21,11 @@ using Xunit;
namespace Server.Tests.Network.Firewall;
[Collection("Sequential UOContent Tests")]
-public class FirewallTests
+public class FirewallTests : IDisposable
{
+ // The test host never configures the firewall, so empty is the state every other test expects.
+ public void Dispose() => Server.Network.Firewall.ResetForTesting();
+
private static IPAddress Ip(string s) => IPAddress.Parse(s);
[Fact]
diff --git a/Projects/UOContent.Tests/Tests/Network/LoginAllowlist/LoginAllowlistTests.cs b/Projects/UOContent.Tests/Tests/Network/LoginAllowlist/LoginAllowlistTests.cs
index 7f3780e58..b48933ec4 100644
--- a/Projects/UOContent.Tests/Tests/Network/LoginAllowlist/LoginAllowlistTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/LoginAllowlist/LoginAllowlistTests.cs
@@ -13,17 +13,74 @@
* along with this program. If not, see . *
*************************************************************************/
+using System;
+using System.Collections;
+using System.Collections.Generic;
using System.Net;
+using System.Reflection;
using Server.Network;
using Server.Network.Bans;
using Xunit;
namespace Server.Tests.Network.LoginAllowlists;
-// The list is static, so every test resets it first. Addresses come from TEST-NET-3 (203.0.113.0/24) so they
-// cannot collide with the blocklist tests if xUnit runs the two classes at the same time.
-public class LoginAllowlistTests
+// The list is static and the login path and BlocklistFilter both read it, so every test resets it first,
+// the class runs in the sequential collection, and Dispose puts the original state back. Addresses come
+// from TEST-NET-3 (203.0.113.0/24).
+[Collection("Sequential UOContent Tests")]
+public class LoginAllowlistTests : IDisposable
{
+ private readonly List<(FieldInfo Field, object Value, DictionaryEntry[] Entries)> _saved = [];
+
+ public LoginAllowlistTests()
+ {
+ var fields = typeof(LoginAllowlist).GetFields(BindingFlags.Static | BindingFlags.NonPublic);
+ for (var i = 0; i < fields.Length; i++)
+ {
+ var field = fields[i];
+ if (field.IsLiteral)
+ {
+ continue;
+ }
+
+ var value = field.GetValue(null);
+
+ // Readonly tables cannot be reassigned, so their contents are copied and refilled instead.
+ DictionaryEntry[] entries = null;
+ if (field.IsInitOnly && value is IDictionary table)
+ {
+ entries = new DictionaryEntry[table.Count];
+ table.CopyTo(entries, 0);
+ }
+ else if (field.IsInitOnly)
+ {
+ continue;
+ }
+
+ _saved.Add((field, value, entries));
+ }
+ }
+
+ public void Dispose()
+ {
+ for (var i = 0; i < _saved.Count; i++)
+ {
+ var (field, value, entries) = _saved[i];
+ if (entries == null)
+ {
+ field.SetValue(null, value);
+ continue;
+ }
+
+ var table = (IDictionary)value;
+ table.Clear();
+ for (var j = 0; j < entries.Length; j++)
+ {
+ table.Add(entries[j].Key, entries[j].Value);
+ }
+ }
+ }
+
private const long Ttl = 90 * 24 * 60 * 60; // 90 days, the shipped default
private const long Window = 3600;
private const long Now = 1_800_000_000;
diff --git a/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs b/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs
index 2fb0f73cb..062fba315 100644
--- a/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Net;
using Server.Accounting;
using Server.Accounting.Security;
@@ -22,14 +23,43 @@ public class AuthIdTests : IDisposable
IncomingAccountPackets.ClearAuthIdWindow();
}
+ // The constructor registers each account in the process-wide Accounts table.
+ private readonly List _created = [];
+
public void Dispose()
{
+ for (var i = 0; i < _created.Count; i++)
+ {
+ _created[i].Delete();
+ }
+
IncomingAccountPackets.ClearAuthIdWindow();
AccountSecurity.CurrentAlgorithm = _originalAlgorithm;
}
- private static IAccount CreateAccount(string username) =>
- Accounts.GetAccount(username) ?? new Account(username, "hunter2");
+ // Auth ids never read the password, so these accounts skip the Argon2 cost.
+ private IAccount CreateAccount(string username)
+ {
+ AccountSecurity.CurrentAlgorithm = PasswordProtectionAlgorithm.SHA2;
+ try
+ {
+ return TrackNewAccount(username);
+ }
+ finally
+ {
+ AccountSecurity.CurrentAlgorithm = PasswordProtectionAlgorithm.Argon2;
+ }
+ }
+
+ // The game-login tests run the real password check, so they keep the configured algorithm.
+ private IAccount CreateArgon2Account(string username) => TrackNewAccount(username);
+
+ private Account TrackNewAccount(string username)
+ {
+ var account = new Account(username, "hunter2");
+ _created.Add(account);
+ return account;
+ }
private static int Register(IAccount account, IPAddress address) =>
IncomingAccountPackets.RegisterAuthId(account, address, new ClientVersion(7, 0, 0, 0));
@@ -336,7 +366,7 @@ public class AuthIdTests : IDisposable
[Fact]
public void PreAuthenticatedGameLogin_SkipsThePasswordCheck()
{
- var account = CreateAccount("authid-preauth-user");
+ var account = CreateArgon2Account("authid-preauth-user");
using var ns = PacketTestUtilities.CreateTestNetState();
// A wrong password is accepted only because the auth id already vouched for the account.
@@ -349,7 +379,7 @@ public class AuthIdTests : IDisposable
[Fact]
public void GameLoginWithoutPreAuthentication_StillChecksThePassword()
{
- var account = CreateAccount("authid-nopreauth-user");
+ var account = CreateArgon2Account("authid-nopreauth-user");
using var ns = PacketTestUtilities.CreateTestNetState();
var wrong = new GameServer.GameLoginEventArgs(ns, account.Username, "wrong-password", false);
diff --git a/Projects/UOContent.Tests/Tests/WorldSaves/ArchiveJournalTests.cs b/Projects/UOContent.Tests/Tests/WorldSaves/ArchiveJournalTests.cs
index 5cf7dbeea..14db8d19d 100644
--- a/Projects/UOContent.Tests/Tests/WorldSaves/ArchiveJournalTests.cs
+++ b/Projects/UOContent.Tests/Tests/WorldSaves/ArchiveJournalTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Reflection;
using Server.Saves;
using Xunit;
@@ -9,16 +10,32 @@ namespace Server.Tests;
[Collection("Sequential UOContent Tests")]
public class ArchiveJournalTests : IDisposable
{
+ private static readonly FieldInfo _dataField =
+ typeof(ArchiveJournal).GetField("_data", BindingFlags.Static | BindingFlags.NonPublic);
+
+ private static readonly FieldInfo _journalPathField =
+ typeof(ArchiveJournal).GetField("_journalPath", BindingFlags.Static | BindingFlags.NonPublic);
+
+ private readonly object _savedData;
+ private readonly object _savedJournalPath;
private readonly string _testDir;
public ArchiveJournalTests()
{
+ // Configure() repoints the process-wide journal at the temp dir; restore it so no later
+ // caller writes into a deleted directory.
+ _savedData = _dataField.GetValue(null);
+ _savedJournalPath = _journalPathField.GetValue(null);
+
_testDir = Path.Combine(Path.GetTempPath(), $"modernuo-journal-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_testDir);
}
public void Dispose()
{
+ _dataField.SetValue(null, _savedData);
+ _journalPathField.SetValue(null, _savedJournalPath);
+
if (Directory.Exists(_testDir))
{
Directory.Delete(_testDir, true);