#W# Merge: Merged with upstream. Conflicts resolved.

This commit is contained in:
WarrentyExpired 2026-10-27 19:56:03 -04:00
commit f6f50470a9
276 changed files with 6742 additions and 8845 deletions

View file

@ -1,41 +1,28 @@
using Server.Tests.Terrain;
using Xunit;
namespace Server.Tests;
/// <summary>
/// Fixture for all server tests. Attempts to load TileData from client files if available.
/// Configure client path via MODERNUO_CLIENT_PATH environment variable or place files at C:\Ultima Online Classic.
/// Fixture for all server tests. The host runs on generated tile data and terrain (see <see cref="SyntheticWorld"/>).
/// </summary>
[CollectionDefinition("Sequential Server Tests", DisableParallelization = true)]
public class ServerFixture : ICollectionFixture<ServerFixture>
{
/// <summary>
/// True if TileData was successfully loaded from client files.
/// </summary>
public static bool TileDataLoaded => TestServerInitializer.TileDataLoaded;
/// <summary>A tile with the Surface flag.</summary>
public static ushort SurfaceTileId => TestTiles.Floor;
/// <summary>
/// A tile ID known to have the Surface flag. 0 if not found.
/// </summary>
public static ushort SurfaceTileId => TestServerInitializer.SurfaceTileId;
/// <summary>A tile with the Impassable flag.</summary>
public static ushort ImpassableTileId => TestTiles.Rock;
/// <summary>
/// A tile ID known to have the Impassable flag. 0 if not found.
/// </summary>
public static ushort ImpassableTileId => TestServerInitializer.ImpassableTileId;
/// <summary>A tile with the Wet flag (water).</summary>
public static ushort WetTileId => TestTiles.Water;
/// <summary>
/// A tile ID known to have the Wet flag (water). 0 if not found.
/// </summary>
public static ushort WetTileId => TestServerInitializer.WetTileId;
/// <summary>
/// A tile ID known to have both Surface and Impassable flags (tables, furniture). 0 if not found.
/// </summary>
public static ushort SurfaceImpassableTileId => TestServerInitializer.SurfaceImpassableTileId;
/// <summary>A tile with both Surface and Impassable flags (tables, furniture).</summary>
public static ushort SurfaceImpassableTileId => TestTiles.Counter;
// Global init runs exactly once via the shared, guarded TestServerInitializer. The single
// bootstrap owns global state for the lifetime of the test host, so there is no
// per-collection teardown — matching UOContent.Tests' TestServerInitializer pattern.
public ServerFixture() => TestServerInitializer.Initialize(loadTileData: true);
public ServerFixture() => TestServerInitializer.Initialize();
}

View file

@ -1,9 +1,9 @@
using System;
using System.IO;
using System.Reflection;
using System.Threading;
using Server.Items;
using Server.Tests.Maps;
using Server.Tests.Terrain;
namespace Server.Tests;
@ -13,51 +13,18 @@ namespace Server.Tests;
/// </summary>
internal static class TestServerInitializer
{
private const string DefaultDataDirectory = @"C:\Ultima Online Classic";
private static bool _initialized;
private static readonly Lock _lock = new();
/// <summary>
/// True if TileData was successfully loaded from client files.
/// </summary>
public static bool TileDataLoaded { get; private set; }
/// <summary>
/// A tile ID known to have the Surface flag. 0 if not found.
/// </summary>
public static ushort SurfaceTileId { get; private set; }
/// <summary>
/// A tile ID known to have the Impassable flag. 0 if not found.
/// </summary>
public static ushort ImpassableTileId { get; private set; }
/// <summary>
/// A tile ID known to have the Wet flag (water). 0 if not found.
/// </summary>
public static ushort WetTileId { get; private set; }
/// <summary>
/// A tile ID known to have both Surface and Impassable flags (tables, furniture). 0 if not found.
/// </summary>
public static ushort SurfaceImpassableTileId { get; private set; }
/// <summary>
/// Initializes the test server. Safe to call multiple times - only initializes once.
/// </summary>
/// <param name="loadTileData">If true, attempts to load TileData from client files.</param>
public static void Initialize(bool loadTileData = false)
public static void Initialize()
{
lock (_lock)
{
if (_initialized)
{
// Already initialized, but check if we need to load TileData now
if (loadTileData && !TileDataLoaded && TileData.MaxItemValue == 0)
{
TryLoadTileData();
DetectTileIds();
}
return;
}
@ -66,11 +33,7 @@ internal static class TestServerInitializer
// Load Configurations
ServerConfiguration.Load(true);
// Try to load TileData if requested
if (loadTileData)
{
TryLoadTileData();
}
SyntheticWorld.LoadData();
// Load an empty assembly list into the resolver
ServerConfiguration.AssemblyDirectories.Add(Core.BaseDirectory);
@ -93,6 +56,7 @@ internal static class TestServerInitializer
// Configure / Initialize
TestMapDefinitions.ConfigureTestMapDefinitions();
SyntheticWorld.StampTerrain();
// Configure the world
World.Configure();
@ -102,90 +66,9 @@ internal static class TestServerInitializer
World.ExitSerializationThreads();
// Detect tile IDs if TileData was loaded
if (loadTileData)
{
DetectTileIds();
}
DecayScheduler.Configure();
_initialized = true;
}
}
private static void TryLoadTileData()
{
var dataDir = GetDataDirectory();
if (string.IsNullOrEmpty(dataDir) || !Directory.Exists(dataDir))
{
return;
}
ServerConfiguration.DataDirectories.Add(dataDir);
var tileDataPath = Core.FindDataFile("tiledata.mul", false);
if (File.Exists(tileDataPath))
{
TileData.Load();
}
}
private static void DetectTileIds()
{
if (TileData.MaxItemValue == 0)
{
return;
}
SurfaceTileId = FindTileWithFlag(TileFlag.Surface);
ImpassableTileId = FindTileWithFlag(TileFlag.Impassable);
WetTileId = FindTileWithFlag(TileFlag.Wet);
SurfaceImpassableTileId = FindTileWithFlags(TileFlag.Surface | TileFlag.Impassable);
TileDataLoaded = SurfaceTileId > 0 && ImpassableTileId > 0 && WetTileId > 0;
}
private static ushort FindTileWithFlag(TileFlag flag)
{
for (ushort i = 1; i <= TileData.MaxItemValue && i < 0xFFFF; i++)
{
var data = TileData.ItemTable[i];
if ((data.Flags & flag) != 0)
{
return i;
}
}
return 0;
}
private static ushort FindTileWithFlags(TileFlag flags)
{
for (ushort i = 1; i <= TileData.MaxItemValue && i < 0xFFFF; i++)
{
var data = TileData.ItemTable[i];
if ((data.Flags & flags) == flags)
{
return i;
}
}
return 0;
}
private static string GetDataDirectory()
{
// Check environment variable first
var envDir = System.Environment.GetEnvironmentVariable("MODERNUO_CLIENT_PATH");
if (!string.IsNullOrEmpty(envDir) && Directory.Exists(envDir))
{
return envDir;
}
// Fall back to default directory
if (Directory.Exists(DefaultDataDirectory))
{
return DefaultDataDirectory;
}
return null;
}
}

View file

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
namespace Server.Tests;
/// <summary>
/// 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.
/// </summary>
internal sealed class EntityCleanup : IDisposable
{
private readonly List<IEntity> _entities = [];
public T Track<T>(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();
}
}

View file

@ -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<int, Mobile> _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; }

View file

@ -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;
/// <summary>
/// Shared plumbing for tests that drive a real <see cref="NetState"/> over loopback.
/// </summary>
public static class NetTestHelpers
{
public const byte PingPacketId = 0x73;
/// <summary>The payload byte of the last ping delivered to the handler from <see cref="RegisterPing"/>.</summary>
public static byte LastPing { get; private set; }
/// <summary>
/// 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.
/// </summary>
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*<NetState, SpanReader, void> onReceive
)
{
var previous = IncomingPackets.GetHandler(packetId);
IncomingPackets.Register(packetId, length, false, onReceive);
return new HandlerRegistration(packetId, previous);
}
/// <summary>Registers a two-byte ping whose payload byte is recorded in <see cref="LastPing"/>.</summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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})"
);
}
/// <summary>Runs the network slice until <paramref name="done"/> holds, failing with the condition that timed out.</summary>
public static void SliceUntil(
Func<bool> 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);
}
}
/// <summary>Reads exactly <paramref name="length"/> bytes from the peer, slicing the server while it waits.</summary>
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;
}
/// <summary>Waits for the server's FIN to reach the peer, failing if any data arrives first.</summary>
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;
}
}
}
}

View file

@ -0,0 +1,31 @@
using System;
using Server.Network;
namespace Server.Tests.Network;
public static class PacketAssert
{
/// <summary>
/// Asserts that <paramref name="send"/> writes exactly the bytes of the reference <paramref name="expected"/>
/// packet to a fresh test NetState negotiated with <paramref name="changes"/>.
/// </summary>
public static void Sends(Packet expected, Action<NetState> send, ProtocolChanges changes = ProtocolChanges.None) =>
Sends(_ => expected, send, changes);
/// <summary>
/// As <see cref="Sends(Packet, Action{NetState}, ProtocolChanges)"/>, for a reference packet that depends on
/// the negotiated NetState. It is built after <paramref name="changes"/> is applied and before the send.
/// </summary>
public static void Sends(
Func<NetState, Packet> expected, Action<NetState> 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);
}
}

View file

@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace Server.Tests;
/// <summary>
/// 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.
/// </summary>
internal sealed class PersistencePipelineScope : IDisposable
{
private readonly List<Persistence> _persistences = [];
private readonly SerializationThreadWorker[] _previousWorkers;
private readonly Assembly[] _previousAssemblies;
private readonly DateTime _previousSaveStart;
private readonly bool _installedWorkers;
private readonly bool _swappedAssemblies;
/// <param name="workerCount">Workers to create, each with an allocated heap.</param>
/// <param name="installWorkers">Install the workers as World's, which WriteSnapshot reads segments from.</param>
/// <param name="visibleAssembly">An assembly the loader must resolve types from by hash.</param>
/// <param name="directoryPrefix">Creates a unique scratch directory under the temp path when set.</param>
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; }
/// <summary>The scratch directory, or null when none was requested.</summary>
public string Directory { get; }
/// <summary>Tracks a persistence so it leaves the static registry on dispose.</summary>
public T Track<T>(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();
}
}
/// <summary>Mirrors World.PauseSerializationThreads: publish the partial chunk, then wait for the drain.</summary>
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);
}
}
}

View file

@ -0,0 +1,42 @@
using System;
using Server.Items;
namespace Server.Tests;
/// <summary>
/// Lets a test move the game clock (<see cref="Core.Now"/>) and restores it on dispose, so a failed
/// assert cannot leave every later test running in the future.
/// </summary>
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;
}
/// <summary>
/// Steps the clock from <see cref="Now"/> through <paramref name="duration"/>, driving the decay
/// scheduler each step without the shared timer wheel, and stops early once <paramref name="item"/>
/// is deleted.
/// </summary>
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;
}

View file

@ -0,0 +1,82 @@
using System;
using System.Buffers;
using Server.Network;
using Server.Tests.Network;
namespace Server.Tests;
/// <summary>
/// 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.
/// </summary>
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<byte> 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<byte> 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<byte> expected = stackalloc byte[5];
var writer = new SpanWriter(expected);
writer.Write(packetId);
writer.Write(serial);
return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
}
}

View file

@ -0,0 +1,17 @@
using Server.Items;
namespace Server.Tests;
/// <summary>
/// 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.
/// </summary>
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;
}

View file

@ -5,10 +5,10 @@
<RootNamespace>Server.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.SkippableFact" Version="1.5.85" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0">
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -26,4 +26,8 @@
<Target Name="CopyData" AfterTargets="AfterBuild">
<Copy SourceFiles="@(DataFiles)" DestinationFolder="$(OutDir)\Data\%(RecursiveDir)" />
</Target>
<!-- The test host writes its generated tile data here at runtime (SyntheticWorld), so Clean does not track it. -->
<Target Name="CleanSyntheticData" AfterTargets="Clean">
<RemoveDir Directories="$(OutDir)\SyntheticData" />
</Target>
</Project>

View file

@ -0,0 +1,306 @@
using System;
using System.Collections.Generic;
namespace Server.Tests.Terrain;
/// <summary>
/// A named scene stamped once into the shared synthetic terrain. Tests that rely on one call
/// <see cref="AssertPresent"/> first, so a missing scene fails instead of passing on flat default ground.
/// </summary>
public sealed class Prefab
{
private readonly Action<Prefab> _stamp;
private readonly Func<Prefab, bool> _isPresent;
public Prefab(string name, Rectangle2D bounds, Point3D anchor, Action<Prefab> stamp, Func<Prefab, bool> isPresent)
{
Name = name;
Bounds = bounds;
Anchor = anchor;
_stamp = stamp;
_isPresent = isPresent;
}
public string Name { get; }
public Rectangle2D Bounds { get; }
/// <summary>The cell a test starts from, at its standing Z.</summary>
public Point3D Anchor { get; }
/// <summary>A second named cell, for scenes with a start and a goal.</summary>
public Point3D Target { get; init; }
internal void Stamp() => _stamp(this);
public void AssertPresent()
{
if (!_isPresent(this))
{
throw new InvalidOperationException($"Prefab {Name} is not stamped");
}
}
}
/// <summary>
/// The shared scenes. Each lives in its own part of the region (256, 256)-(1280, 1280), and they never
/// overlap. <see cref="MapEdgeSea"/> is the exception: it has to touch the map edge. Bespoke terrain belongs
/// outside that region; only tests about prefabs themselves stamp over one.
/// </summary>
public static class Prefabs
{
private static bool _stamped;
// Sea to the west at Z -5, grass to the east at Z 0. Anchor: the first grass cell east of the water.
public static Prefab Coastline { get; } = new(
"Coastline",
new Rectangle2D(256, 256, 32, 32),
new Point3D(272, 272, 0),
p =>
{
TestTerrain.FillLand(new Rectangle2D(p.Bounds.X, p.Bounds.Y, 16, 32), TestTiles.SeaWater, -5);
TestTerrain.FillLand(new Rectangle2D(p.Bounds.X + 16, p.Bounds.Y, 16, 32), TestTiles.Grass, 0);
},
p => IsLand(p.Anchor.X, p.Anchor.Y, TestTiles.Grass, 0) && IsLand(p.Anchor.X - 1, p.Anchor.Y, TestTiles.SeaWater, -5)
);
// Open water 64 tiles square, ringed by the default land. Anchor: the middle.
public static Prefab DeepSea { get; } = new(
"DeepSea",
new Rectangle2D(320, 256, 64, 64),
new Point3D(352, 288, -5),
p => TestTerrain.FillLand(p.Bounds, TestTiles.SeaWater, -5),
p => IsLand(p.Bounds.X, p.Bounds.Y, TestTiles.SeaWater, -5) &&
IsLand(p.Bounds.End.X - 1, p.Bounds.End.Y - 1, TestTiles.SeaWater, -5) &&
IsLand(p.Anchor.X, p.Anchor.Y, TestTiles.SeaWater, -5)
);
// Open water against the west edge of the map. Anchor: 13 tiles in from the edge.
public static Prefab MapEdgeSea { get; } = new(
"MapEdgeSea",
new Rectangle2D(0, 176, 48, 48),
new Point3D(13, 200, -5),
p => TestTerrain.FillLand(p.Bounds, TestTiles.SeaWater, -5),
p => IsLand(0, p.Anchor.Y, TestTiles.SeaWater, -5) && IsLand(p.Bounds.End.X - 1, p.Anchor.Y, TestTiles.SeaWater, -5)
);
// Two walled buildings on open ground: one with a doorway and a counter inside, one with a window, and a
// ramp climbing two Z a tile to a walled deck at Z 20 over open ground, so its cells stand at two Zs.
// Anchor: the ground just outside the first building's doorway.
public static Prefab Town { get; } = new(
"Town",
new Rectangle2D(448, 256, 32, 32),
new Point3D(455, 271, 0),
p =>
{
int x0 = p.Bounds.X, y0 = p.Bounds.Y;
WallRing(x0 + 2, y0 + 2, x0 + 13, y0 + 13, 0, x0 + 7, y0 + 13);
for (var x = x0 + 4; x <= x0 + 10; x++)
{
TestTerrain.AddStatic(x, y0 + 6, TestTiles.Counter, 0);
}
WallRing(x0 + 24, y0 + 20, x0 + 29, y0 + 25, 0, x0 + 26, y0 + 20);
TestTerrain.ClearStatics(x0 + 29, y0 + 22);
TestTerrain.AddStatic(x0 + 29, y0 + 22, TestTiles.Window, 0);
// The ramp runs north up x0 + 17 from y0 + 20 (Z 2) to y0 + 11 (Z 20), onto the deck.
for (var i = 0; i < 10; i++)
{
TestTerrain.AddStatic(x0 + 17, y0 + 20 - i, TestTiles.Floor, (sbyte)(2 + 2 * i));
}
for (var x = x0 + 15; x <= x0 + 20; x++)
{
for (var y = y0 + 2; y <= y0 + 10; y++)
{
var edge = x == x0 + 15 || x == x0 + 20 || y == y0 + 2 || y == y0 + 10;
var ramp = x == x0 + 17 && y == y0 + 10;
TestTerrain.AddStatic(x, y, edge && !ramp ? TestTiles.Wall : TestTiles.Floor, 20);
}
}
},
p => IsStatic(p.Bounds.X + 2, p.Bounds.Y + 2, TestTiles.Wall, 0) &&
IsStatic(p.Bounds.X + 17, p.Bounds.Y + 5, TestTiles.Floor, 20)
);
// Impassable rock crossed by a T of bridge tiles. Anchor: on the bridge.
public static Prefab Causeway { get; } = new(
"Causeway",
new Rectangle2D(512, 256, 32, 32),
new Point3D(516, 272, 1),
p =>
{
int x0 = p.Bounds.X, y0 = p.Bounds.Y;
TestTerrain.FillLand(p.Bounds, TestTiles.RockLand, 0);
for (var x = x0; x < x0 + 32; x++)
{
for (var y = y0 + 15; y <= y0 + 17; y++)
{
TestTerrain.AddStatic(x, y, TestTiles.Bridge, 0);
}
}
for (var y = y0; y < y0 + 15; y++)
{
for (var x = x0 + 15; x <= x0 + 17; x++)
{
TestTerrain.AddStatic(x, y, TestTiles.Bridge, 0);
}
}
},
p => IsLand(p.Bounds.X, p.Bounds.Y, TestTiles.RockLand, 0) && IsStatic(p.Anchor.X, p.Anchor.Y, TestTiles.Bridge, 0)
);
// Impassable rock under a square loop of walkway three tiles wide at Z 5, the only ground to stand on.
// Anchor: on the walkway.
public static Prefab SewerWalkway { get; } = new(
"SewerWalkway",
new Rectangle2D(576, 256, 32, 32),
new Point3D(579, 271, 5),
p =>
{
int x0 = p.Bounds.X, y0 = p.Bounds.Y;
TestTerrain.FillLand(p.Bounds, TestTiles.RockLand, 0);
for (var x = x0 + 2; x <= x0 + 29; x++)
{
for (var y = y0 + 2; y <= y0 + 29; y++)
{
var onLoop = x <= x0 + 4 || x >= x0 + 27 || y <= y0 + 4 || y >= y0 + 27;
if (onLoop)
{
TestTerrain.AddStatic(x, y, TestTiles.Floor, 5);
}
}
}
},
p => IsLand(p.Bounds.X, p.Bounds.Y, TestTiles.RockLand, 0) && IsStatic(p.Anchor.X, p.Anchor.Y, TestTiles.Floor, 5)
);
// A walled upper floor at Z 20 with an L-shaped counter between the south and north halves; the only way
// round is past the east end. Anchor: south of the counter. Target: north of it.
public static Prefab InnUpstairs { get; } = new(
"InnUpstairs",
new Rectangle2D(640, 256, 24, 24),
new Point3D(648, 271, 20),
p =>
{
int x0 = p.Bounds.X, y0 = p.Bounds.Y;
for (var x = x0; x < x0 + 24; x++)
{
for (var y = y0; y < y0 + 24; y++)
{
var edge = x == x0 || x == x0 + 23 || y == y0 || y == y0 + 23;
TestTerrain.AddStatic(x, y, edge ? TestTiles.Wall : TestTiles.Floor, 20);
}
}
for (var x = x0 + 1; x <= x0 + 16; x++)
{
TestTerrain.AddStatic(x, y0 + 11, TestTiles.Counter, 20);
}
for (var y = y0 + 12; y <= y0 + 14; y++)
{
TestTerrain.AddStatic(x0 + 16, y, TestTiles.Counter, 20);
}
},
p => IsStatic(p.Bounds.X + 8, p.Bounds.Y + 11, TestTiles.Counter, 20) &&
IsStatic(p.Anchor.X, p.Anchor.Y, TestTiles.Floor, 20)
)
{
Target = new Point3D(649, 262, 20)
};
public static IReadOnlyList<Prefab> All { get; } =
[Coastline, DeepSea, MapEdgeSea, Town, Causeway, SewerWalkway, InnUpstairs];
public static Prefab Named(string name)
{
for (var i = 0; i < All.Count; i++)
{
if (All[i].Name == name)
{
return All[i];
}
}
throw new ArgumentException($"No prefab named {name}", nameof(name));
}
/// <summary>Stamps every prefab once, then refuses terrain writes outside a <see cref="TestTerrain.Scope"/>.</summary>
public static void StampAll()
{
if (_stamped)
{
return;
}
for (var i = 0; i < All.Count; i++)
{
for (var j = i + 1; j < All.Count; j++)
{
if (Overlaps(All[i].Bounds, All[j].Bounds))
{
throw new InvalidOperationException($"Prefabs {All[i].Name} and {All[j].Name} overlap");
}
}
}
for (var i = 0; i < All.Count; i++)
{
All[i].Stamp();
}
_stamped = true;
TestTerrain.Sealed = true;
}
// A ring of Wall from (x1, y1) to (x2, y2) inclusive, with one open cell at (gapX, gapY).
private static void WallRing(int x1, int y1, int x2, int y2, sbyte z, int gapX, int gapY)
{
for (var x = x1; x <= x2; x++)
{
for (var y = y1; y <= y2; y++)
{
var edge = x == x1 || x == x2 || y == y1 || y == y2;
if (edge && (x != gapX || y != gapY))
{
TestTerrain.AddStatic(x, y, TestTiles.Wall, z);
}
}
}
}
private static bool Overlaps(Rectangle2D a, Rectangle2D b) =>
a.Start.X < b.End.X && b.Start.X < a.End.X && a.Start.Y < b.End.Y && b.Start.Y < a.End.Y;
// Both facets must hold the tile; a scene on only one of them is not present.
private static bool IsLand(int x, int y, short id, sbyte z)
{
var felucca = Map.Felucca.Tiles.GetLandTile(x, y);
var trammel = Map.Trammel.Tiles.GetLandTile(x, y);
return felucca.ID == id && felucca.Z == z && trammel.ID == id && trammel.Z == z;
}
private static bool IsStatic(int x, int y, ushort id, sbyte z) =>
HasStatic(Map.Felucca, x, y, id, z) && HasStatic(Map.Trammel, x, y, id, z);
private static bool HasStatic(Map map, int x, int y, ushort id, sbyte z)
{
var cell = map.Tiles.GetStaticBlock(x >> 3, y >> 3)[x & 7][y & 7];
for (var i = 0; i < cell.Length; i++)
{
if (cell[i].ID == id && cell[i].Z == z)
{
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,108 @@
using System;
using System.IO;
using System.Text;
namespace Server.Tests.Terrain;
/// <summary>
/// Writes <see cref="TestTiles"/> as a pre-HS tiledata.mul, the smallest layout <see cref="TileData"/> reads:
/// 0x4000 land records of 26 bytes and 0x4000 item records of 37 bytes, with a 4-byte header before every 32.
/// The file must stay under 1,644,544 bytes or TileData.Load picks the 7.0 layout.
/// </summary>
public static class SyntheticTileData
{
private const int LandCount = 0x4000;
private const int ItemCount = 0x4000;
private const int NameLength = 20;
public static byte[] Build(Action<TileFlag[]> mutateItemFlags = null)
{
var landFlags = new TileFlag[LandCount];
var landNames = new string[LandCount];
for (var i = 0; i < TestTiles.Land.Count; i++)
{
var tile = TestTiles.Land[i];
landFlags[tile.Id] = tile.Flags;
landNames[tile.Id] = tile.Name;
}
var itemFlags = new TileFlag[ItemCount];
var itemNames = new string[ItemCount];
var itemHeights = new byte[ItemCount];
for (var i = 0; i < TestTiles.Items.Count; i++)
{
var tile = TestTiles.Items[i];
itemFlags[tile.Id] = tile.Flags;
itemNames[tile.Id] = tile.Name;
itemHeights[tile.Id] = tile.Height;
}
mutateItemFlags?.Invoke(itemFlags);
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
for (var i = 0; i < LandCount; i++)
{
if ((i & 0x1F) == 0)
{
writer.Write(0); // header
}
writer.Write((uint)landFlags[i]);
writer.Write((short)0); // texture
WriteName(writer, landNames[i]);
}
for (var i = 0; i < ItemCount; i++)
{
if ((i & 0x1F) == 0)
{
writer.Write(0); // header
}
writer.Write((uint)itemFlags[i]);
writer.Write((byte)0); // weight
writer.Write((byte)0); // quality
writer.Write((ushort)0); // animation
writer.Write((byte)0);
writer.Write((byte)0); // quantity
writer.Write(0);
writer.Write((byte)0);
writer.Write((byte)0); // value
writer.Write(itemHeights[i]);
WriteName(writer, itemNames[i]);
}
writer.Flush();
return stream.ToArray();
}
/// <summary>Writes tiledata.mul into <paramref name="directory"/> and returns its path.</summary>
public static string WriteTo(string directory, Action<TileFlag[]> mutateItemFlags = null)
{
var path = Path.Combine(directory, "tiledata.mul");
File.WriteAllBytes(path, Build(mutateItemFlags));
return path;
}
/// <summary>Makes <paramref name="directory"/> the only data directory and loads its tiledata.mul.</summary>
public static void LoadInto(string directory)
{
ServerConfiguration.DataDirectories.Clear();
ServerConfiguration.DataDirectories.Add(directory);
TileData.Load();
}
private static void WriteName(BinaryWriter writer, string name)
{
Span<byte> buffer = stackalloc byte[NameLength];
buffer.Clear();
if (name != null)
{
Encoding.ASCII.GetBytes(name, buffer);
}
writer.Write(buffer);
}
}

View file

@ -0,0 +1,27 @@
using System.IO;
namespace Server.Tests.Terrain;
/// <summary>
/// Boots a test host on generated data instead of client files, so every machine and CI see the same world.
/// </summary>
public static class SyntheticWorld
{
/// <summary>Where the generated data files are written: the test host's output, overwritten every run.</summary>
public static string DataDirectory => Path.Combine(Core.BaseDirectory, "SyntheticData");
/// <summary>
/// Writes and loads the generated tiledata.mul from <see cref="DataDirectory"/>, which becomes the only data
/// directory, and seeds the multi footprints. Run before anything reads <see cref="TileData"/>.
/// </summary>
public static void LoadData()
{
Directory.CreateDirectory(DataDirectory);
SyntheticTileData.WriteTo(DataDirectory);
SyntheticTileData.LoadInto(DataDirectory);
TestMultis.Seed();
}
/// <summary>Stamps the prefab scenes. Run once Felucca and Trammel are registered.</summary>
public static void StampTerrain() => Prefabs.StampAll();
}

View file

@ -0,0 +1,63 @@
using System.Collections.Generic;
namespace Server.Tests.Terrain;
/// <summary>
/// Invented footprints for the multi IDs content looks up through <see cref="MultiData"/>
/// (HouseFoundation, BaseBoat and the pathing tests). Only the IDs are real; the shapes are not.
/// Needs tile data loaded first: <see cref="MultiComponentList"/> masks IDs with TileData.MaxItemValue.
/// </summary>
public static class TestMultis
{
public const int GuildHouse = 0x74;
public const int Tower = 0x7A;
public const int SmallBoatNorth = 0x0;
// MultiComponentList drops every entry after the first whose flags are 0.
private const TileFlag Visible = TileFlag.Background;
private const short HouseFloorZ = 1;
public static void Seed()
{
MultiData.SetComponents(GuildHouse, WalledHouse(3, doorwayOnSouth: true));
MultiData.SetComponents(Tower, WalledHouse(4, doorwayOnSouth: false));
MultiData.SetComponents(SmallBoatNorth, Boat());
}
// A square of Floor ringed by Wall, (2 * radius + 1) tiles a side, with a one-tile doorway of Floor
// in the middle of the south wall, or of the east wall. The floor sits one Z above the multi so it is
// clear of flat ground (MultiMaskCache.ComputeFootprintClean) yet still one step up from it.
private static MultiComponentList WalledHouse(int radius, bool doorwayOnSouth)
{
var entries = new List<MultiTileEntry>();
for (var x = -radius; x <= radius; x++)
{
for (var y = -radius; y <= radius; y++)
{
var edge = x == -radius || x == radius || y == -radius || y == radius;
var doorway = doorwayOnSouth ? x == 0 && y == radius : x == radius && y == 0;
var id = edge && !doorway ? TestTiles.Wall : TestTiles.Floor;
entries.Add(new MultiTileEntry(id, (short)x, (short)y, HouseFloorZ, Visible));
}
}
return new MultiComponentList(entries);
}
// A deck of Floor three tiles wide and seven long, with Wall rails down both sides.
private static MultiComponentList Boat()
{
var entries = new List<MultiTileEntry>();
for (var x = -2; x <= 2; x++)
{
for (var y = -3; y <= 3; y++)
{
var id = x == -2 || x == 2 ? TestTiles.Wall : TestTiles.Floor;
entries.Add(new MultiTileEntry(id, (short)x, (short)y, 0, Visible));
}
}
return new MultiComponentList(entries);
}
}

View file

@ -0,0 +1,215 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using Server.Engines.Pathing.Cache;
namespace Server.Tests.Terrain;
/// <summary>
/// Stamps land and statics into the synthetic terrain. Felucca and Trammel share one terrain, so every write
/// goes to both. Once <see cref="Prefabs.StampAll"/> has run, writes are only allowed inside a
/// <see cref="Scope"/>, which puts back every block it touched when disposed.
/// </summary>
public static class TestTerrain
{
// Blocks this class allocated, keyed by matrix and block. Anything else is the matrix's shared default
// block, which must never be written to.
private static readonly Dictionary<(TileMatrix, int, int), LandTile[]> _land = [];
private static readonly Dictionary<(TileMatrix, int, int), StaticTile[][][]> _statics = [];
private static TerrainScope _scope;
internal static bool Sealed { get; set; }
/// <param name="caller">Filled in by the compiler; names the opener if the scope is left open.</param>
/// <param name="callerFile">Filled in by the compiler.</param>
public static TerrainScope Scope([CallerMemberName] string caller = null, [CallerFilePath] string callerFile = null)
{
if (_scope != null)
{
throw new InvalidOperationException(
$"A terrain scope opened by {_scope.Opener} is still open; dispose it (a using) first."
);
}
// Cached step masks describe the terrain as it was; drop them so nothing serves the old cells.
StepCache.Instance.Clear();
return _scope = new TerrainScope($"{Path.GetFileNameWithoutExtension(callerFile)}.{caller}");
}
public static void SetLand(int x, int y, short id, sbyte z)
{
var facets = Facets();
for (var i = 0; i < facets.Length; i++)
{
LandBlock(facets[i].Tiles, x >> 3, y >> 3)[((y & 7) << 3) + (x & 7)] = new LandTile(id, z);
}
}
public static void FillLand(Rectangle2D area, short id, sbyte z)
{
for (var x = area.Start.X; x < area.End.X; x++)
{
for (var y = area.Start.Y; y < area.End.Y; y++)
{
SetLand(x, y, id, z);
}
}
}
public static void AddStatic(int x, int y, ushort id, sbyte z)
{
var facets = Facets();
for (var i = 0; i < facets.Length; i++)
{
var column = StaticBlock(facets[i].Tiles, x >> 3, y >> 3)[x & 7];
column[y & 7] = [..column[y & 7], new StaticTile(id, z)];
}
}
public static void ClearStatics(int x, int y)
{
var facets = Facets();
for (var i = 0; i < facets.Length; i++)
{
StaticBlock(facets[i].Tiles, x >> 3, y >> 3)[x & 7][y & 7] = [];
}
}
internal static void EndScope(TerrainScope scope)
{
foreach (var ((tiles, bx, by), original) in scope.Land)
{
if (original == null)
{
_land.Remove((tiles, bx, by));
}
else
{
_land[(tiles, bx, by)] = original;
}
tiles.SetLandBlock(bx, by, original);
}
foreach (var ((tiles, bx, by), original) in scope.Statics)
{
if (original == null)
{
_statics.Remove((tiles, bx, by));
}
else
{
_statics[(tiles, bx, by)] = original;
}
tiles.SetStaticBlock(bx, by, original);
}
_scope = null;
StepCache.Instance.Clear();
}
private static Map[] Facets()
{
if (Sealed && _scope == null)
{
throw new InvalidOperationException("Stamp terrain inside TestTerrain.Scope() so it is put back afterwards.");
}
// A test may query the cache and then stamp more; masks built before the stamp must not survive it.
if (_scope != null)
{
StepCache.Instance.Clear();
}
return [Map.Felucca, Map.Trammel];
}
private static LandTile[] LandBlock(TileMatrix tiles, int bx, int by)
{
var key = (tiles, bx, by);
_land.TryGetValue(key, out var owned);
// The scope keeps the block as it was and the stamp goes into a copy, so disposing is a swap back.
if (_scope != null && !_scope.Land.ContainsKey(key))
{
_scope.Land[key] = owned;
owned = (LandTile[])owned?.Clone();
}
else if (owned != null)
{
return owned;
}
var block = owned ?? new LandTile[64];
_land[key] = block;
tiles.SetLandBlock(bx, by, block);
return block;
}
private static StaticTile[][][] StaticBlock(TileMatrix tiles, int bx, int by)
{
var key = (tiles, bx, by);
_statics.TryGetValue(key, out var owned);
if (_scope != null && !_scope.Statics.ContainsKey(key))
{
_scope.Statics[key] = owned;
owned = owned == null ? null : CopyStatics(owned);
}
else if (owned != null)
{
return owned;
}
var block = owned ?? CopyStatics(tiles.EmptyStaticBlock);
_statics[key] = block;
tiles.SetStaticBlock(bx, by, block);
return block;
}
private static StaticTile[][][] CopyStatics(StaticTile[][][] source)
{
var copy = new StaticTile[8][][];
for (var x = 0; x < 8; x++)
{
copy[x] = new StaticTile[8][];
for (var y = 0; y < 8; y++)
{
copy[x][y] = source[x][y];
}
}
return copy;
}
}
/// <summary>
/// Bespoke terrain for one test. Dispose it (a <c>using</c>) to put back every block it touched.
/// </summary>
public sealed class TerrainScope : IDisposable
{
// The block each key held before this scope first wrote to it; null means it was the default.
internal Dictionary<(TileMatrix, int, int), LandTile[]> Land { get; } = [];
internal Dictionary<(TileMatrix, int, int), StaticTile[][][]> Statics { get; } = [];
private bool _disposed;
internal TerrainScope(string opener) => Opener = opener;
/// <summary>The test that opened this scope, as Class.Method.</summary>
public string Opener { get; }
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
TestTerrain.EndScope(this);
}
}

View file

@ -0,0 +1,61 @@
using System.Collections.Generic;
namespace Server.Tests.Terrain;
public readonly record struct TestItemTile(ushort Id, string Name, TileFlag Flags, byte Height);
public readonly record struct TestLandTile(short Id, string Name, TileFlag Flags);
/// <summary>
/// The invented tiles the generated tiledata.mul holds. Every other entry in the file is zero.
/// IDs are arbitrary and carry no meaning from the client, except the content-referenced land below.
/// </summary>
public static class TestTiles
{
public const ushort Wall = 0x3000;
public const ushort Floor = 0x3001;
public const ushort Counter = 0x3002;
public const ushort Stair = 0x3003;
public const ushort Bridge = 0x3004;
public const ushort Roof = 0x3005;
public const ushort Door = 0x3006;
public const ushort Window = 0x3007;
public const ushort Water = 0x3008;
public const ushort Rock = 0x3009;
public const ushort Plain = 0x300A;
public const short Grass = 0x3000;
public const short RockLand = 0x3001;
public const short Dirt = 0x3002;
// Fishing only treats these land IDs as water (Fishing.cs _waterLandTiles), so they keep their numbers.
public const short SeaWater = 0xA8;
public static IReadOnlyList<TestItemTile> Items { get; } =
[
new(Wall, "test wall", TileFlag.Wall | TileFlag.Impassable | TileFlag.NoShoot, 20),
new(Floor, "test floor", TileFlag.Surface | TileFlag.Background, 0),
new(Counter, "test counter", TileFlag.Surface | TileFlag.Impassable, 4),
new(Stair, "test stair", TileFlag.Bridge | TileFlag.Surface | TileFlag.StairBack, 5),
new(Bridge, "test bridge", TileFlag.Bridge | TileFlag.Surface, 2),
new(Roof, "test roof", TileFlag.Roof | TileFlag.Impassable, 0),
new(Door, "test door", TileFlag.Door | TileFlag.Impassable | TileFlag.NoShoot, 20),
new(Window, "test window", TileFlag.Window | TileFlag.Wall | TileFlag.Impassable, 20),
new(Water, "test water", TileFlag.Wet | TileFlag.Impassable | TileFlag.Surface, 0),
new(Rock, "test rock", TileFlag.Impassable, 20),
new(Plain, "test plain", TileFlag.None, 1)
];
public static IReadOnlyList<TestLandTile> Land { get; } =
[
new(Grass, "test grass", TileFlag.None),
new(RockLand, "test rock", TileFlag.Impassable),
new(Dirt, "test dirt", TileFlag.None),
new(0xA8, "test water", TileFlag.Wet | TileFlag.Impassable),
new(0xA9, "test water", TileFlag.Wet | TileFlag.Impassable),
new(0xAA, "test water", TileFlag.Wet | TileFlag.Impassable),
new(0xAB, "test water", TileFlag.Wet | TileFlag.Impassable),
new(0x136, "test water", TileFlag.Wet | TileFlag.Impassable),
new(0x137, "test water", TileFlag.Wet | TileFlag.Impassable)
];
}

View file

@ -56,25 +56,6 @@ public class RawInterpolatedStringHandlerTests
handler.Clear();
}
[Fact]
public void TestNoFormatPreservesCase()
{
var handler = new RawInterpolatedStringHandler(0, 1);
handler.AppendFormatted("Hello WORLD");
Assert.Equal("Hello WORLD", handler.Text.ToString());
handler.Clear();
}
[Fact]
public void TestUnicodeLowercase()
{
// ToLowerInvariant on Greek letter
var handler = new RawInterpolatedStringHandler(0, 1);
handler.AppendFormatted("ΑΒΓ", format: "L");
Assert.Equal("αβγ", handler.Text.ToString());
handler.Clear();
}
[Fact]
public void TestLowercaseFormatSurrogatePairAtChunkBoundary()
{

View file

@ -497,60 +497,6 @@ public class SpanReaderTests
Assert.Equal(3, reader.Position);
}
[Fact]
public void TestLength()
{
ReadOnlySpan<byte> buffer = [0x01, 0x02, 0x03, 0x04, 0x05];
var reader = new SpanReader(buffer);
Assert.Equal(5, reader.Length);
}
[Fact]
public void TestPosition()
{
ReadOnlySpan<byte> buffer = [0x01, 0x02, 0x03];
var reader = new SpanReader(buffer);
Assert.Equal(0, reader.Position);
reader.ReadByte();
Assert.Equal(1, reader.Position);
reader.ReadUInt16();
Assert.Equal(3, reader.Position);
}
[Fact]
public void TestRemaining()
{
ReadOnlySpan<byte> buffer = [0x01, 0x02, 0x03, 0x04, 0x05];
var reader = new SpanReader(buffer);
Assert.Equal(5, reader.Remaining);
reader.ReadByte();
Assert.Equal(4, reader.Remaining);
reader.ReadUInt16();
Assert.Equal(2, reader.Remaining);
reader.ReadUInt16();
Assert.Equal(0, reader.Remaining);
}
[Fact]
public void TestBuffer()
{
ReadOnlySpan<byte> buffer = [0x01, 0x02, 0x03];
var reader = new SpanReader(buffer);
var bufferProperty = reader.Buffer;
Assert.Equal(3, bufferProperty.Length);
AssertThat.Equal(bufferProperty, buffer);
}
[Fact]
public void TestReadStringEmptyFixedLength()
{

View file

@ -75,19 +75,6 @@ public class SpanWriterTests
AssertThat.Equal(writer.Span, [1, 0]);
}
[Fact]
public void TestWriteByte()
{
Span<byte> buffer = stackalloc byte[2];
var writer = new SpanWriter(buffer);
writer.Write((byte)0x12);
writer.Write((byte)0x34);
Assert.Equal(2, writer.Position);
AssertThat.Equal(writer.Span, [0x12, 0x34]);
}
[Fact]
public void TestWriteSByte()
{
@ -235,19 +222,6 @@ public class SpanWriterTests
AssertThat.Equal(writer.Span, [0x01, 0x02, 0x03, 0x01, 0x02, 0x03]);
}
[Fact]
public void TestWriteAsciiChar()
{
Span<byte> buffer = stackalloc byte[2];
var writer = new SpanWriter(buffer);
writer.WriteAscii('A');
writer.WriteAscii('B');
Assert.Equal(2, writer.Position);
AssertThat.Equal(writer.Span, "AB"u8);
}
[Fact]
public void TestWriteAsciiString()
{
@ -457,16 +431,6 @@ public class SpanWriterTests
writer.Dispose();
}
[Fact]
public void TestEnsureCapacityNoResize()
{
Span<byte> buffer = stackalloc byte[10];
var writer = new SpanWriter(buffer);
writer.EnsureCapacity(10);
Assert.Equal(10, writer.Capacity);
}
[Fact]
public void TestEnsureCapacityNoResizeThrows()
{
@ -552,40 +516,4 @@ public class SpanWriterTests
writer.Write((byte)0xFF);
Assert.Equal(3, writer.BytesWritten);
}
[Fact]
public void TestCapacity()
{
Span<byte> buffer = stackalloc byte[10];
var writer = new SpanWriter(buffer);
Assert.Equal(10, writer.Capacity);
}
[Fact]
public void TestRawBuffer()
{
Span<byte> buffer = stackalloc byte[10];
var writer = new SpanWriter(buffer);
writer.Write((byte)0x01);
var raw = writer.RawBuffer;
Assert.Equal(10, raw.Length);
Assert.Equal(0x01, raw[0]);
}
[Fact]
public void TestSpan()
{
Span<byte> buffer = stackalloc byte[10];
var writer = new SpanWriter(buffer);
writer.Write((byte)0x01);
writer.Write((byte)0x02);
var span = writer.Span;
Assert.Equal(2, span.Length);
AssertThat.Equal(span, [0x01, 0x02]);
}
}

View file

@ -52,19 +52,6 @@ public class ValueStringBuilderTests
// writes into the shared buffer, and Append() reconciles via `this = handler._builder`.
// Critical to detect if C# compiler codegen changes break this assumption.
[Fact]
public void Interpolation_Stackalloc_NoGrow()
{
// Stackalloc buffer large enough — no Grow needed.
// Validates: copy's Span shares same stackalloc memory, _length is reconciled.
var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append($"Hello {42} world");
Assert.Equal("Hello 42 world", sb.ToString());
Assert.Equal(14, sb.Length);
sb.Dispose();
}
[Fact]
public void Interpolation_Stackalloc_WithGrow()
{
@ -82,20 +69,6 @@ public class ValueStringBuilderTests
sb.Dispose();
}
[Fact]
public void Interpolation_Stackalloc_PreExistingContent_NoGrow()
{
// Append plain text first, then interpolation. Buffer large enough.
// Validates: interpolation appends after existing content, doesn't overwrite.
var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append("prefix-");
sb.Append($"value={99}");
Assert.Equal("prefix-value=99", sb.ToString());
Assert.Equal(15, sb.Length);
sb.Dispose();
}
[Fact]
public void Interpolation_Stackalloc_PreExistingContent_WithGrow()
{
@ -264,20 +237,6 @@ public class ValueStringBuilderTests
sb.Dispose();
}
[Fact]
public void Interpolation_Stackalloc_DisposeAfterGrow()
{
// After Grow moves stackalloc to pool, Dispose should return the pooled array.
// Validates: _arrayToReturnToPool is correctly reconciled so Dispose works.
var sb = new ValueStringBuilder(stackalloc char[4]);
sb.Append($"grow beyond stackalloc: {12345}");
// If _arrayToReturnToPool wasn't reconciled, Dispose would either
// not return the pooled array (leak) or return null (no-op when it shouldn't be).
sb.Dispose();
// No assertion needed — if _arrayToReturnToPool was wrong, pool corruption
// would surface as test failures elsewhere. The test passing = no crash.
}
[Fact]
public void Interpolation_Heap_DoubleGrow()
{

View file

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

View file

@ -16,13 +16,6 @@ public class ValueLinkListTests
public TestEntity(int serial) => Serial = serial;
}
[Fact]
public void TestDefaultListIsEmpty()
{
var linkList = new ValueLinkList<TestEntity>();
Assert.Equal(0, linkList.Count);
}
[Fact]
public void TestAddingElementsFirst()
{
@ -397,29 +390,4 @@ public class ValueLinkListTests
}
);
}
[Fact]
public void TestThrowsIfModifiedWhileIteratingNested()
{
Assert.Throws<InvalidOperationException>(
() =>
{
var linkList = new ValueLinkList<TestEntity>();
var entity1 = new TestEntity(1);
var entity2 = new TestEntity(2);
linkList.AddFirst(entity1);
linkList.AddLast(entity2);
foreach (var item in linkList)
{
foreach (var nestedItem in linkList)
{
linkList.Remove(nestedItem);
}
}
}
);
}
}

View file

@ -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);
}
}

View file

@ -16,31 +16,6 @@ public class Point2DTests
Assert.Equal($"({int.MaxValue}, {int.MinValue})", new Point2D(int.MaxValue, int.MinValue).ToString());
}
[Fact]
public void TestPoint2DTryFormatSucceeds()
{
var array = new char[128];
var p1 = new Point2D(0, 0);
Assert.True(p1.TryFormat(array, out var cp1, null, null));
Assert.Equal(6, cp1);
Array.Clear(array);
var p2 = new Point2D(1, 1);
Assert.True(p2.TryFormat(array, out var cp2, null, null));
Assert.Equal(6, cp2);
Array.Clear(array);
var p3 = new Point2D(int.MaxValue, int.MaxValue);
Assert.True(p3.TryFormat(array, out var cp3, null, null));
Assert.Equal(4 + 20, cp3);
Array.Clear(array);
var p4 = new Point2D(int.MinValue, int.MinValue);
Assert.True(p4.TryFormat(array, out var cp4, null, null));
Assert.Equal(4 + 22, cp4);
}
[Fact]
public void TestPoint2DTryFormatFails()
{

View file

@ -27,33 +27,6 @@ public class Point3DTests
Assert.Equal($"({min}, {min}, {min})", new Point3D(min, min, min).ToString());
}
[Fact]
public void TestPoint3DTryFormatSucceeds()
{
const int max = int.MaxValue;
const int min = int.MinValue;
var array = new char[128];
var p1 = new Point3D(0, 0, 0);
Assert.True(p1.TryFormat(array, out var cp1, null, null));
Assert.Equal(6 + 3, cp1);
Array.Clear(array);
var p2 = new Point3D(1, 1, 1);
Assert.True(p2.TryFormat(array, out var cp2, null, null));
Assert.Equal(6 + 3, cp2);
Array.Clear(array);
var p3 = new Point3D(max, max, max);
Assert.True(p3.TryFormat(array, out var cp3, null, null));
Assert.Equal(6 + 30, cp3);
Array.Clear(array);
var p4 = new Point3D(min, min, min);
Assert.True(p4.TryFormat(array, out var cp4, null, null));
Assert.Equal(6 + 33, cp4);
}
[Fact]
public void TestPoint3DTryFormatFails()
{
@ -95,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<FormatException>(() => Point3D.Parse("101, 23, 55)", null));

View file

@ -16,33 +16,6 @@ public class Rectangle2DTests
Assert.Equal($"({min}, {min})+({min}, {min})", new Rectangle2D(min, min, min, min).ToString());
}
[Fact]
public void TestRectangle2DTryFormatSucceeds()
{
const int max = int.MaxValue;
const int min = int.MinValue;
var array = new char[128];
var p1 = new Rectangle2D(0, 0, 0, 0);
Assert.True(p1.TryFormat(array, out var cp1, null, null));
Assert.Equal(9 + 4, cp1);
Array.Clear(array);
var p2 = new Rectangle2D(1, 1, 1, 1);
Assert.True(p2.TryFormat(array, out var cp2, null, null));
Assert.Equal(9 + 4, cp2);
Array.Clear(array);
var p3 = new Rectangle2D(max, max, max, max);
Assert.True(p3.TryFormat(array, out var cp3, null, null));
Assert.Equal(9 + 4 * 10, cp3);
Array.Clear(array);
var p4 = new Rectangle2D(min, min, min, min);
Assert.True(p4.TryFormat(array, out var cp4, null, null));
Assert.Equal(9 + 4 * 11, cp4);
}
[Fact]
public void TestRectangle2DTryFormatFails()
{

View file

@ -16,33 +16,6 @@ public class Rectangle3DTests
Assert.Equal($"({min}, {min}, {min})+({min}, {min}, {min})", new Rectangle3D(min, min, min, min, min, min).ToString());
}
[Fact]
public void TestRectangle3DTryFormatSucceeds()
{
const int max = int.MaxValue;
const int min = int.MinValue;
var array = new char[128];
var p1 = new Rectangle3D(0, 0, 0, 0, 0, 0);
Assert.True(p1.TryFormat(array, out var cp1, null, null));
Assert.Equal(13 + 6, cp1);
Array.Clear(array);
var p2 = new Rectangle3D(1, 1, 1, 1, 1, 1);
Assert.True(p2.TryFormat(array, out var cp2, null, null));
Assert.Equal(13 + 6, cp2);
Array.Clear(array);
var p3 = new Rectangle3D(max, max, max, max, max, max);
Assert.True(p3.TryFormat(array, out var cp3, null, null));
Assert.Equal(13 + 6 * 10, cp3);
Array.Clear(array);
var p4 = new Rectangle3D(min, min, min, min, min, min);
Assert.True(p4.TryFormat(array, out var cp4, null, null));
Assert.Equal(13 + 6 * 11, cp4);
}
[Fact]
public void TestRectangle3DTryFormatFails()
{
@ -131,7 +104,7 @@ public class Rectangle3DTests
Assert.False(Rectangle3D.TryParse("(101,,10)+(55, 89 1)", null, out p));
Assert.Equal(default, p);
Assert.Throws<FormatException>(() => Rectangle3D.Parse("(,23,)+(,89)", null));
Assert.Throws<FormatException>(() => Rectangle3D.Parse("(101,,10)+(55, 89 1)", null));
Assert.False(Rectangle3D.TryParse("(,,)+(55,89, 1)", null, out p));
Assert.Equal(default, p);

View file

@ -39,40 +39,6 @@ public sealed class WorldLocationTests
Assert.Equal($"({min}, {min}, {min}) [{_superLongMapName}]", new WorldLocation(min, min, min, map).ToString());
}
[Theory]
[InlineData(null)]
[InlineData("Internal Map")]
[InlineData(_superLongMapName)]
public void TestWorldLocationTryFormatSucceeds(string mapName)
{
var map = mapName != null ? CreateMap(mapName) : null;
var mapNameLength = mapName?.Length ?? _nullMap.Length;
const int max = int.MaxValue;
const int min = int.MinValue;
var array = new char[128];
var p1 = new WorldLocation(0, 0, 0, map);
Assert.True(p1.TryFormat(array, out var cp1, null, null));
Assert.Equal(9 + 3 + mapNameLength, cp1);
Array.Clear(array);
var p2 = new WorldLocation(1, 1, 1, map);
Assert.True(p2.TryFormat(array, out var cp2, null, null));
Assert.Equal(9 + 3 + mapNameLength, cp2);
Array.Clear(array);
array = new char[256]; // We need a bigger buffer!
var p3 = new WorldLocation(max, max, max, map);
Assert.True(p3.TryFormat(array, out var cp3, null, null));
Assert.Equal(9 + 30 + mapNameLength, cp3);
Array.Clear(array);
var p4 = new WorldLocation(min, min, min, map);
Assert.True(p4.TryFormat(array, out var cp4, null, null));
Assert.Equal(9 + 33 + mapNameLength, cp4);
}
[Theory]
[InlineData(null)]
[InlineData("Internal Map")]
@ -124,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<FormatException>(() => 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<FormatException>(() => 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<FormatException>(() => WorldLocation.Parse("101, 23, 55) [Felucca]", null));

View file

@ -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();
}
}

View file

@ -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<byte> 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<byte> 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);

View file

@ -66,27 +66,6 @@ public class ContainerChildSkipTests
}
// A skipped child is not saved, so the container must load without it.
[Fact]
public void SkippingContainer_LoadsWithoutItsSkippedChildren()
{
var pack = new SkippingContainer();
var item = new Item(0x1234);
pack.DropItem(item);
var writer = new BufferWriter(true);
pack.Serialize(writer);
var buffer = new byte[writer.Position];
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
item.Delete();
pack.Delete();
var copy = new SkippingContainer(World.NewItem);
copy.Deserialize(new BufferReader(buffer));
Assert.Empty(copy.Items);
}
// A written child serial could resolve at load to an unrelated item that was handed the same serial.
[Fact]
public void SkippingContainer_DoesNotWriteAliveChildSerial()

View file

@ -419,15 +419,6 @@ public class ContainerTests
Assert.Equal(0, c.GetBestGroupAmount(new[] { typeof(TestReagentA) }, true, HueGrouper));
}
[Fact]
public void TestGetBestGroupAmount_NullGrouper_Throws()
{
var c = MakeContainer(0x2C0);
Assert.Throws<ArgumentNullException>(
() => c.GetBestGroupAmount(new[] { typeof(TestReagentA) }, true, null)
);
}
[Fact]
public void TestGetAmount_TypeArray_SumsAcrossTypes()
{
@ -470,39 +461,6 @@ public class ContainerTests
Assert.Equal(1, s2.Amount);
}
[Fact]
public void TestConsumeTotal_LengthMismatch_Throws()
{
var c = MakeContainer(0x300);
Assert.Throws<ArgumentException>(
() => c.ConsumeTotal(new[] { typeof(TestReagentA) }, new[] { 1, 2 })
);
}
[Fact]
public void TestConsumeTotalGrouped_LengthMismatch_Throws()
{
var c = MakeContainer(0x310);
Assert.Throws<ArgumentException>(
() => c.ConsumeTotalGrouped(
new[] { typeof(TestReagentA) }, new[] { 1, 2 },
true, null, HueGrouper
)
);
}
[Fact]
public void TestConsumeTotalGrouped_NullGrouper_Throws()
{
var c = MakeContainer(0x320);
Assert.Throws<ArgumentNullException>(
() => c.ConsumeTotalGrouped(
new[] { typeof(TestReagentA) }, new[] { 1 },
true, null, null
)
);
}
// -------------------------------------------------------------------------
// FindItemsByType(Type) and FindItemsByType(ReadOnlySpan<Type>) — Phase 10:
// these used to allocate a Predicate<Item> per call (method-group conversion

View file

@ -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,156 +24,44 @@ public class DecayRegistrationTests
public override bool OnDecay() => false;
}
// The item is already dequeued, so a refusal that does not reschedule strands it forever.
[Fact]
public void ItemWhoseDecayIsRefused_StaysTrackedAndSurvives()
{
var start = Core._now;
try
{
var item = new RefusesDecayItem();
item.MoveToWorld(new Point3D(106, 100, 0), Map.Felucca);
AdvanceDecay(start, 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 so it is retried."
);
item.Delete();
}
finally
{
Core._now = start;
}
}
// 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.");
item.Delete();
}
finally
{
Core._now = start;
}
}
[Fact]
public void ItemPlacedInWorld_IsRegisteredForDecay()
{
var item = new Item(0x1234);
item.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
Assert.True(item.CanDecay());
Assert.True(DecayScheduler.IsRegistered(item), "Item on the ground must be tracked for decay.");
Assert.False(item.Deleted, "Item must not decay before DecayTime elapses.");
item.Delete();
}
// The real client path: Mobile.Lift internalizes, then Mobile.Drop calls MoveToWorld.
[Fact]
public void ItemLiftedThenDroppedToGround_IsRegisteredForDecay()
{
var item = new Item(0x1234);
item.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
// Player lifts the item to the cursor.
item.Internalize();
Assert.Equal(Map.Internal, item.Map);
// Player drops it back onto the ground.
item.MoveToWorld(new Point3D(101, 100, 0), Map.Felucca);
Assert.True(item.CanDecay());
Assert.True(
DecayScheduler.IsRegistered(item),
"Item dropped to the ground after being lifted must be tracked for decay."
);
item.Delete();
}
// Held on the cursor means Map.Internal, which must not be tracked.
[Fact]
public void ItemLiftedToCursor_IsNotRegisteredForDecay()
{
var item = new Item(0x1234);
item.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
item.Internalize();
Assert.False(item.CanDecay());
Assert.False(DecayScheduler.IsRegistered(item), "Item held on the cursor must not be tracked for decay.");
item.Delete();
}
// A new item is on Map.Internal, so construction must not touch the scheduler.
[Fact]
public void NewItem_IsNotTracked()
{
var item = new Item(0x1234);
Assert.Equal(DecayScheduler.SlotNone, item.DecaySlot);
Assert.False(DecayScheduler.IsRegistered(item), "A newly constructed item must not be tracked.");
item.Delete();
}
// Unregister() trusts DecaySlot; IsRegistered() scans the structures. Assert both to catch drift.
[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);
@ -193,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));
@ -213,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
@ -377,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)
@ -407,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
@ -440,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;
@ -456,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);
@ -473,26 +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.
[Fact]
public void ItemMovedIntoContainerThenBackToGround_IsRegisteredForDecay()
{
var pack = new Container(0xE75);
pack.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
var item = new Item(0x1234);
pack.AddItem(item);
Assert.False(DecayScheduler.IsRegistered(item), "Item inside a container must not be tracked for decay.");
item.Internalize();
item.MoveToWorld(new Point3D(101, 100, 0), Map.Felucca);
Assert.True(item.CanDecay());
Assert.True(DecayScheduler.IsRegistered(item), "Item taken out of a container must be tracked for decay.");
item.Delete();
pack.Delete();
}
}

View file

@ -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<byte> 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<byte> 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<byte> 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()
{

View file

@ -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();
}
}

View file

@ -24,25 +24,6 @@ public class PlayerConstructedStackingTests
private static StackableItem MakeStack(Serial serial, int amount, bool playerConstructed) =>
new(serial) { Amount = amount, PlayerConstructed = playerConstructed };
[Theory]
[InlineData(false)]
[InlineData(true)]
public void CanStackWith_IsTrueWhenProvenanceMatches(bool playerConstructed)
{
var first = MakeStack((Serial)0x1, 5, playerConstructed);
var second = MakeStack((Serial)0x2, 7, playerConstructed);
try
{
Assert.True(first.CanStackWith(second));
}
finally
{
first.Delete();
second.Delete();
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]

View file

@ -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<byte> 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;
}
}
}

View file

@ -1,30 +0,0 @@
using Server.Json;
using Xunit;
namespace Server.Tests.Json;
public class JsonDiscoverableTypeAttributeTests
{
[JsonDiscoverableType]
private sealed class DefaultName { }
[JsonDiscoverableType("custom")]
private sealed class Overridden { }
[Fact]
public void DefaultDiscriminator_IsNull()
{
var attr = (JsonDiscoverableTypeAttribute)System.Attribute.GetCustomAttribute(
typeof(DefaultName), typeof(JsonDiscoverableTypeAttribute), false);
Assert.NotNull(attr);
Assert.Null(attr.Discriminator);
}
[Fact]
public void OverrideDiscriminator_IsReturned()
{
var attr = (JsonDiscoverableTypeAttribute)System.Attribute.GetCustomAttribute(
typeof(Overridden), typeof(JsonDiscoverableTypeAttribute), false);
Assert.Equal("custom", attr.Discriminator);
}
}

View file

@ -35,12 +35,4 @@ public class Rectangle3DConverterTests
Assert.DoesNotContain("z1", json);
Assert.DoesNotContain("z2", json);
}
[Fact]
public void HomeRangeBounds_WritesZ()
{
var json = JsonSerializer.Serialize(new Rectangle3D(0, 0, -128, 10, 10, 256), JsonConfig.DefaultOptions);
Assert.Contains("\"z1\":", json);
Assert.Contains("\"z2\":", json);
}
}

View file

@ -1,20 +1,53 @@
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);
}
[Fact]
public void RemovingTheLastFallbackEntry_KeepsLaterAddsVisible()
{
Localization.Clear();
try
{
Localization.Add("enu", 500004, "first");
Assert.True(Localization.Remove("enu", 500004));
Localization.Add("enu", 500005, "second");
Assert.False(Localization.TryGetLocalization(500004, out _));
Assert.True(Localization.TryGetLocalization(500005, out var entry));
Assert.Equal("second", entry.Text);
}
finally
{
// Lets the next lookup reload the real tables from disk.
Localization.Clear();
}
}
}

View file

@ -9,11 +9,6 @@ namespace Server.Tests.Maps;
[Collection("Sequential Server Tests")]
public class CanFitItemTests
{
private void SkipIfNoTileData()
{
Skip.If(!ServerFixture.TileDataLoaded, "TileData not loaded - client files required");
}
[Fact]
public void CanFitItem_InternalMapReturnsFalse()
{
@ -33,6 +28,7 @@ public class CanFitItemTests
Assert.False(map.CanFitItem(100, map.Height + 1, 0, 1));
}
// The synthetic map's default ground: land ID 0 at Z 0 with no flags, a surface with nothing above it.
private const int TestLandX = 1500;
private const int TestLandY = 1600;
@ -59,12 +55,9 @@ public class CanFitItemTests
Assert.False(result, "No surface 50 units above land");
}
[SkippableFact]
[Fact]
public void CanFitItem_SurfaceImpassableMultiIsValidSurface()
{
SkipIfNoTileData();
Skip.If(ServerFixture.SurfaceImpassableTileId == 0, "No Surface+Impassable tile found in TileData");
var map = Map.Felucca;
const int x = 1700;
const int y = 1700;
@ -92,12 +85,9 @@ public class CanFitItemTests
}
}
[SkippableFact]
[Fact]
public void CanFitItem_CanFitDoesNotAllowSurfaceImpassable()
{
SkipIfNoTileData();
Skip.If(ServerFixture.SurfaceImpassableTileId == 0, "No Surface+Impassable tile found in TileData");
var map = Map.Felucca;
const int x = 1750;
const int y = 1750;
@ -128,11 +118,9 @@ public class CanFitItemTests
}
}
[SkippableFact]
[Fact]
public void CanFitItem_NonMovableWorldItemSurfaceWorks()
{
SkipIfNoTileData();
var map = Map.Felucca;
const int x = 1800;
const int y = 1800;
@ -158,11 +146,9 @@ public class CanFitItemTests
}
}
[SkippableFact]
[Fact]
public void CanFitItem_MovableWorldItemNotValidSurface()
{
SkipIfNoTileData();
var map = Map.Felucca;
const int x = 1850;
const int y = 1850;
@ -170,23 +156,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
{
@ -194,11 +176,9 @@ public class CanFitItemTests
}
}
[SkippableFact]
[Fact]
public void CanFitItem_ImpassableTileBlocksPlacement()
{
SkipIfNoTileData();
var map = Map.Felucca;
const int x = 1900;
const int y = 1900;
@ -220,39 +200,6 @@ public class CanFitItemTests
}
}
[SkippableFact]
public void CanFitItem_CanPlaceOnTopOfImpassable()
{
SkipIfNoTileData();
Skip.If(ServerFixture.SurfaceImpassableTileId == 0, "No Surface+Impassable tile found in TileData");
var map = Map.Felucca;
const int x = 1950;
const int y = 1950;
TestMulti multi = null;
try
{
// Get the actual tile height from TileData
var tileData = TileData.ItemTable[ServerFixture.SurfaceImpassableTileId];
var tileHeight = tileData.CalcHeight;
const int tileZ = 10;
// Create a Surface+Impassable tile at Z=10
multi = CreateSurfaceImpassableMulti(map, new Point3D(x, y, 0), surfaceZ: tileZ);
// Place item on top should work
var surfaceTop = tileZ + tileHeight;
var result = map.CanFitItem(x, y, surfaceTop, 1);
Assert.True(result, $"Should be able to place item on top of Surface+Impassable tile at Z={surfaceTop}");
}
finally
{
multi?.Delete();
}
}
private TestMulti CreateSurfaceImpassableMulti(Map map, Point3D location, int surfaceZ)
{
// Use SurfaceImpassableTileId - a tile that has both Surface and Impassable flags
@ -281,16 +228,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;
}
}

View file

@ -3,8 +3,8 @@ using Xunit;
namespace Server.Tests.Maps;
/// <summary>
/// Tests for CanSpawnMobile that don't require TileData (client files).
/// For tests that require client files, see CanSpawnMobileTileDataTests.
/// Tests for CanSpawnMobile on the default ground.
/// For tests over tile flags, see CanSpawnMobileTileDataTests.
/// </summary>
[Collection("Sequential Server Tests")]
public class CanSpawnMobileTests
@ -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
// The synthetic map's default ground: land ID 0 at Z 0 with no flags, which CanSpawnMobile treats as
// 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");
}

View file

@ -4,56 +4,14 @@ using Xunit;
namespace Server.Tests.Maps;
/// <summary>
/// Tests for CanSpawnMobile that require TileData (client files) to be loaded.
/// These tests will be skipped if client files are not available.
/// Configure client path via MODERNUO_CLIENT_PATH environment variable or place files at C:\Ultima Online Classic.
/// Tests for CanSpawnMobile over tile flags, using tiles from the generated test kit.
/// </summary>
[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");
}
[SkippableFact]
public void CanSpawnMobile_FindsSurfaceOnMulti()
{
SkipIfNoTileData();
var map = Map.Felucca;
const int x = 1100;
const int y = 1100;
TestMulti multi = null;
try
{
// Create a multi with a floor at Z=20 using a real surface tile
multi = CreateFloorMulti(map, new Point3D(x, y, 0), floorZ: 20);
var result = map.CanSpawnMobile(x, y, 15, 30, false, false, out var spawnZ);
Assert.True(result);
Assert.True(spawnZ >= 15 && spawnZ <= 30);
}
finally
{
multi?.Delete();
}
}
[SkippableFact]
[Fact]
public void CanSpawnMobile_FindsLowestValidSurface()
{
SkipIfNoTileData();
var map = Map.Felucca;
const int x = 1200;
const int y = 1200;
@ -77,11 +35,9 @@ public class CanSpawnMobileTileDataTests
}
}
[SkippableFact]
[Fact]
public void CanSpawnMobile_ZRangeSelectsCorrectFloor()
{
SkipIfNoTileData();
var map = Map.Felucca;
const int x = 1300;
const int y = 1300;
@ -105,11 +61,9 @@ public class CanSpawnMobileTileDataTests
}
}
[SkippableFact]
[Fact]
public void CanSpawnMobile_LowCeilingBlocksSpawn()
{
SkipIfNoTileData();
var map = Map.Felucca;
const int x = 1400;
const int y = 1400;
@ -122,7 +76,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);
}
@ -132,11 +86,9 @@ public class CanSpawnMobileTileDataTests
}
}
[SkippableFact]
[Fact]
public void CanSpawnMobile_HighCeilingAllowsSpawn()
{
SkipIfNoTileData();
var map = Map.Felucca;
const int x = 1450;
const int y = 1450;
@ -147,7 +99,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);
}
@ -157,11 +109,9 @@ public class CanSpawnMobileTileDataTests
}
}
[SkippableFact]
[Fact]
public void CanSpawnMobile_BlockerOutsideZRangeIgnored()
{
SkipIfNoTileData();
var map = Map.Felucca;
const int x = 1475;
const int y = 1475;
@ -173,7 +123,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);
}
@ -183,11 +133,9 @@ public class CanSpawnMobileTileDataTests
}
}
[SkippableFact]
[Fact]
public void CanSpawnMobile_WaterSurfaceForSwimmingMob()
{
SkipIfNoTileData();
var map = Map.Felucca;
const int x = 1550;
const int y = 1550;
@ -199,7 +147,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);
}
@ -209,11 +157,9 @@ public class CanSpawnMobileTileDataTests
}
}
[SkippableFact]
[Fact]
public void CanSpawnMobile_WorldItemSurfaceWorks()
{
SkipIfNoTileData();
var map = Map.Felucca;
const int x = 1650;
const int y = 1650;
@ -228,7 +174,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);
@ -239,51 +185,6 @@ public class CanSpawnMobileTileDataTests
}
}
[SkippableFact]
public void CanSpawnMobile_MalasBuilding_FindsGroundFloor()
{
SkipIfNoTileData();
var map = Map.Malas;
const int x = 991;
const int y = 519;
var result = map.CanSpawnMobile(x, y, -60, -40, false, false, out var spawnZ);
Assert.True(result);
Assert.True(spawnZ >= -60 && spawnZ <= -40, $"Expected ground floor around -50, got {spawnZ}");
}
[SkippableFact]
public void CanSpawnMobile_MalasBuilding_FindsSecondFloor()
{
SkipIfNoTileData();
var map = Map.Malas;
const int x = 991;
const int y = 519;
var result = map.CanSpawnMobile(x, y, -30, 0, false, false, out var spawnZ);
Assert.True(result);
Assert.True(spawnZ >= -30 && spawnZ <= 0, $"Expected second floor, got {spawnZ}");
}
[SkippableFact]
public void CanSpawnMobile_MalasBuilding_LowestFloorPreferred()
{
SkipIfNoTileData();
var map = Map.Malas;
const int x = 991;
const int y = 519;
var result = map.CanSpawnMobile(x, y, -60, 0, false, false, out var spawnZ);
Assert.True(result);
Assert.True(spawnZ <= -40, $"Expected lowest floor around -50, got {spawnZ}");
}
private TestMulti CreateFloorMulti(Map map, Point3D location, int floorZ)
{
var multi = new TestMulti(CreateFloorComponents(floorZ));
@ -345,16 +246,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;
}
}

View file

@ -308,49 +308,6 @@ public class ClientEnumeratorTests
}
}
[Fact]
public void ClientAtEnumerator_UsesDifferentPoint3DOverloads()
{
var map = Map.Felucca;
var location = new Point3D(800, 800, 5);
var clients = new (NetState, Mobile)[1];
try
{
clients[0] = CreateClientWithMobile(map, location);
// Test Point3D overload
var found1 = new List<NetState>();
foreach (var ns in map.GetClientsAt(location))
{
found1.Add(ns);
}
// Test (int, int) overload - should find the same client (Z is ignored)
var found2 = new List<NetState>();
foreach (var ns in map.GetClientsAt(location.X, location.Y))
{
found2.Add(ns);
}
// Test Point2D overload
var found3 = new List<NetState>();
foreach (var ns in map.GetClientsAt(new Point2D(location.X, location.Y)))
{
found3.Add(ns);
}
Assert.Single(found1);
Assert.Equal(clients[0].Item1, found1[0]);
Assert.Equal(found1, found2);
Assert.Equal(found1, found3);
}
finally
{
DeleteAll(clients);
}
}
[Fact]
public void ClientEnumerator_GetClientsInRange()
{
@ -383,39 +340,6 @@ public class ClientEnumeratorTests
}
}
[Fact]
public void ClientEnumerator_DeletedMobilesAreSkipped()
{
var map = Map.Felucca;
var rect = new Rectangle2D(1000, 1000, 16, 16);
var clients = new (NetState, Mobile)[3];
try
{
clients[0] = CreateClientWithMobile(map, new Point3D(1005, 1005, 0));
clients[1] = CreateClientWithMobile(map, new Point3D(1006, 1005, 0));
clients[2] = CreateClientWithMobile(map, new Point3D(1007, 1005, 0));
// Delete the mobile (but not the NetState)
clients[1].Item2.Delete();
var found = new List<NetState>();
foreach (var ns in map.GetClientsInBounds(rect))
{
found.Add(ns);
}
// Should skip the client whose mobile was deleted
Assert.Equal(2, found.Count);
Assert.Contains(clients[0].Item1, found);
Assert.Contains(clients[2].Item1, found);
}
finally
{
DeleteAll(clients);
}
}
private static (NetState, Mobile) CreateClientWithMobile(Map map, Point3D location)
{
// Create test NetState with real socket and buffers

View file

@ -38,100 +38,6 @@ public class ItemByDistanceEnumeratorTests
}
}
[Fact]
public void ItemByDistanceEnumerator_DeletedItemsAreSkipped()
{
var map = Map.Felucca;
var center = new Point3D(200, 200, 0);
const int range = 5;
var Items = new TestItem[3];
try
{
Items[0] = CreateItem(map, new Point3D(202, 202, 0));
Items[1] = CreateItem(map, new Point3D(203, 202, 0));
Items[2] = CreateItem(map, new Point3D(204, 202, 0));
Items[1].Delete();
var found = new List<Item>();
foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range))
{
found.Add(Item);
}
Assert.Equal(2, found.Count);
Assert.Contains(Items[0], found);
Assert.Contains(Items[2], found);
Assert.DoesNotContain(Items[1], found);
}
finally
{
DeleteAll(Items);
}
}
[Fact]
public void ItemByDistanceEnumerator_ReturnsMinDistance()
{
var map = Map.Felucca;
var center = new Point3D(300, 300, 0);
const int range = 10;
var Items = new TestItem[2];
try
{
Items[0] = CreateItem(map, new Point3D(305, 305, 0));
Items[1] = CreateItem(map, new Point3D(302, 302, 0));
var foundWithDistance = new List<(Item, int)>();
foreach (var result in map.GetItemsInRangeByDistance(center, range))
{
foundWithDistance.Add(result);
}
Assert.Equal(2, foundWithDistance.Count);
// Each Item should have a non-negative min distance
Assert.All(foundWithDistance, item => Assert.True(item.Item2 >= 0));
}
finally
{
DeleteAll(Items);
}
}
[Fact]
public void ItemByDistanceEnumerator_OrderedBySector()
{
var map = Map.Felucca;
var center = new Point3D(400, 400, 0);
const int range = Map.SectorSize * 2;
var Items = new TestItem[3];
try
{
// Place Items in different sectors
Items[0] = CreateItem(map, new Point3D(center.X + 2, center.Y + 2, 0));
Items[1] = CreateItem(map, new Point3D(center.X + Map.SectorSize + 2, center.Y + 2, 0));
Items[2] = CreateItem(map, new Point3D(center.X + 2, center.Y + Map.SectorSize + 2, 0));
var found = new List<Item>();
foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range))
{
found.Add(Item);
}
Assert.Equal(3, found.Count);
Assert.Contains(Items[0], found);
Assert.Contains(Items[1], found);
Assert.Contains(Items[2], found);
}
finally
{
DeleteAll(Items);
}
}
[Fact]
public void ItemByDistanceEnumerator_MapNullYieldsEmpty()
{
@ -239,50 +145,6 @@ public class ItemByDistanceEnumeratorTests
}
}
[Fact]
public void ItemByDistanceEnumerator_UsesDifferentPointOverloads()
{
var map = Map.Felucca;
var center = new Point3D(800, 800, 5);
const int range = 5;
var Items = new TestItem[1];
try
{
Items[0] = CreateItem(map, new Point3D(802, 802, 0));
// Test Point3D overload
var found1 = new List<Item>();
foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range))
{
found1.Add(Item);
}
// Test (int, int) overload
var found2 = new List<Item>();
foreach (var (Item, _) in map.GetItemsInRangeByDistance<Item>(center.X, center.Y, range))
{
found2.Add(Item);
}
// Test Point2D overload
var found3 = new List<Item>();
foreach (var (Item, _) in map.GetItemsInRangeByDistance(new Point2D(center.X, center.Y), range))
{
found3.Add(Item);
}
Assert.Single(found1);
Assert.Equal(Items[0], found1[0]);
Assert.Equal(found1, found2);
Assert.Equal(found1, found3);
}
finally
{
DeleteAll(Items);
}
}
[Fact]
public void ItemByDistanceEnumerator_RingTraversal()
{
@ -384,58 +246,6 @@ public class ItemByDistanceEnumeratorTests
}
}
[Fact]
public void ItemByDistanceEnumerator_MultipleRings()
{
var map = Map.Felucca;
var center = new Point3D(1100, 1100, 0);
const int range = Map.SectorSize * 3;
var Items = new List<TestItem>();
try
{
// Create a grid of Items across multiple sectors
for (var ringOffset = 0; ringOffset <= 2; ringOffset++)
{
for (var side = 0; side < 4; side++)
{
var offset = ringOffset * Map.SectorSize;
var pos = side switch
{
0 => new Point3D(center.X + offset, center.Y, 0),
1 => new Point3D(center.X, center.Y + offset, 0),
2 => new Point3D(center.X - offset, center.Y, 0),
_ => new Point3D(center.X, center.Y - offset, 0)
};
Items.Add(CreateItem(map, pos));
}
}
var found = new List<Item>();
foreach (var (Item, _) in map.GetItemsInRangeByDistance(center, range))
{
found.Add(Item);
}
// Should find all Items within range
Assert.True(found.Count > 0);
Assert.All(found, Item =>
{
var dx = Item.X - center.X;
var dy = Item.Y - center.Y;
var distSq = dx * dx + dy * dy;
Assert.True(distSq <= range * range);
});
}
finally
{
foreach (var Item in Items)
{
Item?.Delete();
}
}
}
[Fact]
public void ItemByDistanceEnumerator_MatchesBoundsEnumeratorAtMapEdge()
{
@ -556,33 +366,6 @@ public class ItemByDistanceEnumeratorTests
}
}
[Fact]
public void ItemByDistanceEnumerator_Bounds_ReturnsMinDistance()
{
var map = Map.Felucca;
var bounds = new Rectangle2D(300, 300, 20, 20);
var Items = new TestItem[2];
try
{
Items[0] = CreateItem(map, new Point3D(305, 305, 0));
Items[1] = CreateItem(map, new Point3D(315, 315, 0));
var foundWithDistance = new List<(Item, int)>();
foreach (var result in map.GetItemsInBoundsByDistance<Item>(bounds))
{
foundWithDistance.Add(result);
}
Assert.Equal(2, foundWithDistance.Count);
Assert.All(foundWithDistance, item => Assert.True(item.Item2 >= 0));
}
finally
{
DeleteAll(Items);
}
}
[Fact]
public void ItemByDistanceEnumerator_Bounds_OrdersByProximityToCenter()
{

View file

@ -256,35 +256,6 @@ public class ItemEnumeratorTests
}
}
[Fact]
public void ItemAtEnumerator_DeletedItemsAreSkipped()
{
var map = Map.Felucca;
var location = new Point3D(650, 650, 0);
var items = new Item[3];
try
{
items[0] = CreateItem(map, location);
items[1] = CreateItem(map, location);
items[2] = CreateItem(map, location);
items[1].Delete();
var found = new List<Item>();
foreach (var item in map.GetItemsAt<Item>(location))
{
found.Add(item);
}
Assert.Equal(new[] { items[0], items[2] }, found);
}
finally
{
DeleteAll(items);
}
}
[Fact]
public void ItemAtEnumerator_ItemsWithParentAreSkipped()
{
@ -365,49 +336,6 @@ public class ItemEnumeratorTests
}
}
[Fact]
public void ItemAtEnumerator_UsesDifferentPoint3DOverloads()
{
var map = Map.Felucca;
var location = new Point3D(800, 800, 5);
var items = new Item[1];
try
{
items[0] = CreateItem(map, location);
// Test Point3D overload
var found1 = new List<Item>();
foreach (var item in map.GetItemsAt(location))
{
found1.Add(item);
}
// Test (int, int) overload - should find the same item (Z is ignored)
var found2 = new List<Item>();
foreach (var item in map.GetItemsAt(location.X, location.Y))
{
found2.Add(item);
}
// Test Point2D overload
var found3 = new List<Item>();
foreach (var item in map.GetItemsAt(new Point2D(location.X, location.Y)))
{
found3.Add(item);
}
Assert.Single(found1);
Assert.Equal(items[0], found1[0]);
Assert.Equal(found1, found2);
Assert.Equal(found1, found3);
}
finally
{
DeleteAll(items);
}
}
[Fact]
public void ItemEnumerator_ZeroRangeReturnsOnlyCenter()
{

View file

@ -16,15 +16,6 @@ namespace Server.Tests.Maps
Assert.Equal(expected, flags.ToCommaDelimitedString());
}
[Fact]
public void TestFormatFeluccaOnlyMapSelection()
{
const MapSelectionFlags flags = MapSelectionFlags.Felucca;
// When, Then
Assert.Equal("Felucca", flags.ToCommaDelimitedString());
}
public class TestMapConfig
{
[JsonConverter(typeof(FlagsConverter<MapSelectionFlags>))]

View file

@ -71,76 +71,6 @@ public class MobileByDistanceEnumeratorTests
}
}
[Fact]
public void MobileByDistanceEnumerator_ReturnsMinDistance()
{
var map = Map.Felucca;
var center = new Point3D(300, 300, 0);
const int range = 10;
var mobiles = new TestMobile[2];
try
{
mobiles[0] = CreateMobile(map, new Point3D(305, 305, 0));
mobiles[1] = CreateMobile(map, new Point3D(302, 302, 0));
var foundWithDistance = new List<(Mobile, int)>();
foreach (var result in map.GetMobilesInRangeByDistance(center, range))
{
foundWithDistance.Add(result);
}
Assert.Equal(2, foundWithDistance.Count);
// Each mobile should have a non-negative min distance
Assert.All(foundWithDistance, item => Assert.True(item.Item2 >= 0));
}
finally
{
DeleteAll(mobiles);
}
}
[Fact]
public void MobileByDistanceEnumerator_OrderedBySector()
{
var map = Map.Felucca;
var center = new Point3D(400, 400, 0);
const int range = Map.SectorSize * 2;
var mobiles = new TestMobile[3];
try
{
// Place mobiles in different sectors
mobiles[0] = CreateMobile(map, new Point3D(center.X + 2, center.Y + 2, 0));
mobiles[1] = CreateMobile(map, new Point3D(center.X + Map.SectorSize + 2, center.Y + 2, 0));
mobiles[2] = CreateMobile(map, new Point3D(center.X + 2, center.Y + Map.SectorSize + 2, 0));
var found = new List<Mobile>();
foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range))
{
found.Add(mobile);
}
Assert.Equal(3, found.Count);
Assert.Contains(mobiles[0], found);
Assert.Contains(mobiles[1], found);
Assert.Contains(mobiles[2], found);
}
finally
{
DeleteAll(mobiles);
}
}
[Fact]
public void MobileByDistanceEnumerator_MapNullYieldsEmpty()
{
var center = new Point2D(0, 0);
var bounds = new Rectangle2D(center.m_X - 10, center.m_Y - 10, 21, 21);
var enumerator = new Map.MobileDistanceEnumerable<Mobile>(null, bounds, center, false).GetEnumerator();
Assert.False(enumerator.MoveNext());
}
[Fact]
public void MobileByDistanceEnumerator_ThrowsOnVersionChange()
{
@ -241,50 +171,6 @@ public class MobileByDistanceEnumeratorTests
}
}
[Fact]
public void MobileByDistanceEnumerator_UsesDifferentPointOverloads()
{
var map = Map.Felucca;
var center = new Point3D(800, 800, 5);
const int range = 5;
var mobiles = new TestMobile[1];
try
{
mobiles[0] = CreateMobile(map, new Point3D(802, 802, 0));
// Test Point3D overload
var found1 = new List<Mobile>();
foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range))
{
found1.Add(mobile);
}
// Test (int, int) overload
var found2 = new List<Mobile>();
foreach (var (mobile, _) in map.GetMobilesInRangeByDistance<Mobile>(center.X, center.Y, range))
{
found2.Add(mobile);
}
// Test Point2D overload
var found3 = new List<Mobile>();
foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(new Point2D(center.X, center.Y), range))
{
found3.Add(mobile);
}
Assert.Single(found1);
Assert.Equal(mobiles[0], found1[0]);
Assert.Equal(found1, found2);
Assert.Equal(found1, found3);
}
finally
{
DeleteAll(mobiles);
}
}
[Fact]
public void MobileByDistanceEnumerator_RingTraversal()
{
@ -386,58 +272,6 @@ public class MobileByDistanceEnumeratorTests
}
}
[Fact]
public void MobileByDistanceEnumerator_MultipleRings()
{
var map = Map.Felucca;
var center = new Point3D(1100, 1100, 0);
const int range = Map.SectorSize * 3;
var mobiles = new List<TestMobile>();
try
{
// Create a grid of mobiles across multiple sectors
for (var ringOffset = 0; ringOffset <= 2; ringOffset++)
{
for (var side = 0; side < 4; side++)
{
var offset = ringOffset * Map.SectorSize;
var pos = side switch
{
0 => new Point3D(center.X + offset, center.Y, 0),
1 => new Point3D(center.X, center.Y + offset, 0),
2 => new Point3D(center.X - offset, center.Y, 0),
_ => new Point3D(center.X, center.Y - offset, 0)
};
mobiles.Add(CreateMobile(map, pos));
}
}
var found = new List<Mobile>();
foreach (var (mobile, _) in map.GetMobilesInRangeByDistance(center, range))
{
found.Add(mobile);
}
// Should find all mobiles within range
Assert.True(found.Count > 0);
Assert.All(found, mobile =>
{
var dx = mobile.X - center.X;
var dy = mobile.Y - center.Y;
var distSq = dx * dx + dy * dy;
Assert.True(distSq <= range * range);
});
}
finally
{
foreach (var mobile in mobiles)
{
mobile?.Delete();
}
}
}
private static TestMobile CreateMobile(Map map, Point3D location)
{
var mobile = new TestMobile(World.NewMobile);
@ -526,33 +360,6 @@ public class MobileByDistanceEnumeratorTests
}
}
[Fact]
public void MobileByDistanceEnumerator_Bounds_ReturnsMinDistance()
{
var map = Map.Felucca;
var bounds = new Rectangle2D(300, 300, 20, 20);
var mobiles = new TestMobile[2];
try
{
mobiles[0] = CreateMobile(map, new Point3D(305, 305, 0));
mobiles[1] = CreateMobile(map, new Point3D(315, 315, 0));
var foundWithDistance = new List<(Mobile, int)>();
foreach (var result in map.GetMobilesInBoundsByDistance<Mobile>(bounds))
{
foundWithDistance.Add(result);
}
Assert.Equal(2, foundWithDistance.Count);
Assert.All(foundWithDistance, item => Assert.True(item.Item2 >= 0));
}
finally
{
DeleteAll(mobiles);
}
}
[Fact]
public void MobileByDistanceEnumerator_Bounds_OrdersByProximityToCenter()
{

View file

@ -87,13 +87,6 @@ public class MobileEnumeratorTests
}
}
[Fact]
public void MobileEnumerator_MapNullYieldsEmpty()
{
var enumerator = new Map.MobileEnumerator<Mobile>(null, Rectangle2D.Empty, false);
Assert.False(enumerator.MoveNext());
}
[Fact]
public void MobileEnumerator_ThrowsOnVersionChange()
{

View file

@ -88,13 +88,6 @@ public class MultiEnumeratorTests
}
}
[Fact]
public void MultiEnumerator_MapNullYieldsEmpty()
{
var enumerator = new Map.MultiBoundsEnumerable<BaseMulti>(null, Rectangle2D.Empty, false).GetEnumerator();
Assert.False(enumerator.MoveNext());
}
[Fact]
public void MultiEnumerator_ThrowsOnVersionChange()
{
@ -250,35 +243,6 @@ public class MultiEnumeratorTests
}
}
[Fact]
public void MultiSectorEnumerator_DeletedMultisAreSkipped()
{
var map = Map.Felucca;
var location = new Point3D(750, 750, 0);
var multis = new TestMulti[3];
try
{
multis[0] = CreateMulti(map, location);
multis[1] = CreateMulti(map, new Point3D(location.X + 1, location.Y, 0));
multis[2] = CreateMulti(map, new Point3D(location.X + 2, location.Y, 0));
multis[1].Delete();
var found = new List<BaseMulti>();
foreach (var multi in map.GetMultisInRange<BaseMulti>(location, 10))
{
found.Add(multi);
}
Assert.Equal(new[] { multis[0], multis[2] }, found);
}
finally
{
DeleteAll(multis);
}
}
[Fact]
public void MultiSectorEnumerator_MapNullYieldsEmpty()
{
@ -286,49 +250,6 @@ public class MultiEnumeratorTests
Assert.False(enumerator.MoveNext());
}
[Fact]
public void MultiSectorEnumerator_UsesDifferentPointOverloads()
{
var map = Map.Felucca;
var location = new Point3D(800, 800, 5);
var multis = new TestMulti[1];
try
{
multis[0] = CreateMulti(map, location);
// Test Point3D overload
var found1 = new List<BaseMulti>();
foreach (var multi in map.GetMultisInSector(location))
{
found1.Add(multi);
}
// Test (int, int) overload
var found2 = new List<BaseMulti>();
foreach (var multi in map.GetMultisInSector(location.X, location.Y))
{
found2.Add(multi);
}
// Test Point2D overload
var found3 = new List<BaseMulti>();
foreach (var multi in map.GetMultisInSector(new Point2D(location.X, location.Y)))
{
found3.Add(multi);
}
Assert.Single(found1);
Assert.Equal(multis[0], found1[0]);
Assert.Equal(found1, found2);
Assert.Equal(found1, found3);
}
finally
{
DeleteAll(multis);
}
}
[Fact]
public void MultiEnumerator_ZeroRangeReturnsOnlyCenter()
{
@ -392,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);
@ -430,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)
]
);
}

View file

@ -7,50 +7,23 @@ namespace Server.Tests.Maps;
[Collection("Sequential Server Tests")]
public class StaticTileEnumeratorTests
{
[Fact]
public void StaticTileEnumerator_MapNullYieldsEmpty()
{
var enumerator = new Map.StaticTileEnumerable(null, new Point2D(0, 0)).GetEnumerator();
Assert.False(enumerator.MoveNext());
}
[Fact]
public void StaticTileEnumerator_EmptyLocationYieldsEmpty()
{
var map = Map.Felucca;
var location = new Point2D(100, 100);
var tiles = new List<StaticTile>();
foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: true, includeMultis: false))
{
tiles.Add(tile);
}
// Since we don't have actual map files loaded, this should be empty
Assert.Empty(tiles);
}
[Fact]
public void StaticTileEnumerator_IncludeStaticsOnlyWorks()
{
var map = Map.Felucca;
var location = new Point2D(200, 200);
// The synthetic map has no statics here; the multi must add nothing to the statics-only walk.
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<StaticTile>();
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
{
@ -92,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<StaticTile>();
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
{
@ -144,24 +116,6 @@ public class StaticTileEnumeratorTests
}
}
[Fact]
public void StaticTileEnumerator_TileMatrixGetStaticTilesWorks()
{
var map = Map.Felucca;
var x = 600;
var y = 600;
// Test the TileMatrix.GetStaticTiles method
var tiles = new List<StaticTile>();
foreach (var tile in map.Tiles.GetStaticTiles(x, y))
{
tiles.Add(tile);
}
// Without map files loaded, should be empty
Assert.Empty(tiles);
}
[Fact]
public void StaticTileEnumerator_TileMatrixGetStaticAndMultiTilesWorks()
{
@ -252,13 +206,6 @@ public class StaticTileEnumeratorTests
}
}
[Fact]
public void StaticTileEnumerator_EmptyReturnsCorrectly()
{
var enumerator = Map.StaticTileEnumerable.Empty.GetEnumerator();
Assert.False(enumerator.MoveNext());
}
[Fact]
public void StaticTileEnumerator_DeletedMultiSkipped()
{
@ -305,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<StaticTile>();
@ -342,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<StaticTile> CollectTiles(Map map, Point2D location, bool includeStatics, bool includeMultis)
{
var tiles = new List<StaticTile>();
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)
]
);
}
}

View file

@ -0,0 +1,79 @@
using Xunit;
namespace Server.Tests.Maps;
[Collection("Sequential Server Tests")]
public class TileMatrixInjectionTests
{
// Not registered in Map.Maps, and no data directory holds map96.mul, so this matrix has no file streams.
private static Map NewFilelessMap() => new(0x60, 0x60, 0x60, 64, 64, 0, "inject-test", MapRules.TrammelRules);
[Fact]
public void InjectedLandBlock_IsReturned_WithNoMapFile()
{
var tiles = NewFilelessMap().Tiles;
var block = new LandTile[64];
block[9] = new LandTile(0x10, 7);
tiles.SetLandBlock(1, 1, block);
var tile = tiles.GetLandTile(9, 9);
Assert.Equal(0x10, tile.ID);
Assert.Equal(7, tile.Z);
}
[Fact]
public void UninjectedLand_IsIdZeroAtZZero()
{
var tile = NewFilelessMap().Tiles.GetLandTile(30, 30);
Assert.Equal(0, tile.ID);
Assert.Equal(0, tile.Z);
}
[Fact]
public void InjectedStaticBlock_IsReturned_WithNoStaticFiles()
{
var tiles = NewFilelessMap().Tiles;
var block = new StaticTile[8][][];
for (var x = 0; x < 8; x++)
{
block[x] = new StaticTile[8][];
for (var y = 0; y < 8; y++)
{
block[x][y] = [];
}
}
block[1][1] = [new StaticTile(0x20, 5)];
tiles.SetStaticBlock(1, 1, block);
var cell = tiles.GetStaticBlock(1, 1)[1][1];
Assert.Single(cell);
Assert.Equal(0x20, cell[0].ID);
Assert.Equal(5, cell[0].Z);
}
[Fact]
public void ClearingABlock_RestoresTheDefault()
{
var tiles = NewFilelessMap().Tiles;
var block = new LandTile[64];
block[9] = new LandTile(0x10, 7);
tiles.SetLandBlock(1, 1, block);
tiles.SetLandBlock(1, 1, null);
Assert.Equal(0, tiles.GetLandTile(9, 9).ID);
}
[Fact]
public void OutOfRange_StillReturnsDefault()
{
var tiles = NewFilelessMap().Tiles;
Assert.Equal(0, tiles.GetLandBlock(-1, 0)[0].ID);
Assert.Equal(0, tiles.GetLandBlock(8, 0)[0].ID);
}
}

View file

@ -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<IPAddress, string, bool> _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]

View file

@ -32,13 +32,4 @@ public class BanConfigurationTests
Assert.Equal(original.ReportRateLimitTrips, restored.ReportRateLimitTrips);
Assert.Equal(original.AutoBanDuration, restored.AutoBanDuration); // TimeSpan survives
}
[Fact]
public void BanSettings_Defaults_AreReportRateLimitTripsFourHourAutoBan()
{
var settings = new BanSettings();
Assert.True(settings.ReportRateLimitTrips);
Assert.Equal(TimeSpan.FromHours(4), settings.AutoBanDuration);
}
}

View file

@ -10,14 +10,6 @@ namespace Server.Tests;
[Collection("Sequential Server Tests")]
public class EventLoopIdleTests
{
[Fact]
public void FreshContextIsEmpty()
{
var context = new EventLoopContext();
Assert.True(context.IsEmpty);
}
[Fact]
public void PostedWorkMakesContextNonEmpty()
{

View file

@ -4,337 +4,20 @@ using Xunit;
namespace Server.Tests.Network;
/// <summary>
/// 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.
/// </summary>
[Collection("Sequential Server Tests")]
[Trait("Category", "Manual")]
public class MovementThrottleLatencyTests
{
/// <summary>
/// Sets the simulated tick count for testing.
/// </summary>
private static void SetTickCount(long ticks)
{
Core._tickCount = ticks;
}
/// <summary>
/// Simulates recording movements at specific intervals.
/// </summary>
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;
}
}
/// <summary>
/// Scenario: Player experiences a 3-second network lag spike, then all buffered
/// packets arrive at once. This should NOT trigger false positive detection.
///
/// Timeline:
/// - T=0-1000ms: Normal movement (10 packets at 100ms intervals)
/// - T=1000-4000ms: Network lag (client buffers packets)
/// - T=4000ms: Network recovers, 30 buffered packets arrive simultaneously
/// </summary>
[Fact]
public void LagSpike_BurstRecovery_NoFalsePositive()
{
var ns = PacketTestUtilities.CreateTestNetState();
SetTickCount(0);
// Phase 1: Normal movement for 1 second
var movements = new (int delayMs, int costMs)[10];
for (var i = 0; i < 10; i++)
{
movements[i] = (100, 100); // Normal mounted running
}
SimulateMovements(ns, movements);
// Verify normal rate before lag
var rateBeforeLag = MovementThrottle.CalculateMovementRate(ns, out var samples);
Assert.True(samples >= 8, $"Expected >= 8 samples, got {samples}");
Assert.True(rateBeforeLag >= 0.95f && rateBeforeLag <= 1.05f,
$"Expected normal rate before lag, got {rateBeforeLag}");
// Phase 2: Simulate 3 second lag followed by burst
// The gap (3000ms) is tracked, then burst packets arrive
var currentTime = Core.TickCount;
// Record the lag gap
SetTickCount(currentTime + 3000);
ns._lastGapDuration = 3000; // Gap > maxChainGap triggers this
// Burst packets arriving (5 packets with tiny intervals)
for (var i = 0; i < 5; i++)
{
SetTickCount(Core.TickCount + 2); // 2ms between packets (simultaneous arrival)
if (ns._lastMovementRecordTime > 0)
{
var interval = (int)(Core.TickCount - ns._lastMovementRecordTime);
ns._movementHistory[ns._movementHistoryIndex] = new NetState.MovementRecord
{
Interval = (short)interval,
TargetSpeed = 100,
QueueDepth = 0,
Flags = 0
};
ns._movementHistoryIndex++;
if (ns._movementHistoryIndex >= 20)
{
ns._movementHistoryIndex = 0;
ns._movementHistoryFull = true;
}
}
ns._lastMovementRecordTime = Core.TickCount;
}
// Analyze - should apply burst forgiveness due to large gap
var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out _, out var confidence);
// The burst will have high rate due to tiny intervals, but:
// 1. _lastGapDuration > 1000 (maxChainGap/2) triggers burst forgiveness
// 2. Confidence should be reduced by 0.3f multiplier
// 3. Should NOT be Definite unless truly extreme
Assert.True(
verdict != MovementThrottle.DetectionVerdict.Definite,
$"Lag recovery should not be Definite. Rate={rate}, Confidence={confidence}, Verdict={verdict}"
);
// Gap duration should be cleared after analysis
Assert.Equal(0, ns._lastGapDuration);
}
/// <summary>
/// Scenario: Player with consistently high latency (400ms RTT) moves normally.
/// Their connection is consistent but slow - should NOT be considered "stable"
/// for the purpose of tightening detection thresholds.
/// </summary>
[Fact]
public void HighLatencyConsistent_NotConsideredStable()
public void AnalyzeMovement_BurstAfterGap_WrappedFullHistory_ForgivesAndClearsGap()
{
var ns = PacketTestUtilities.CreateTestNetState();
// Set up high but consistent RTT
ns._rttHistory = new long[] { 400, 405, 398, 402, 401, 399, 403, 400 };
ns._rttSampleCount = 8;
// Calculate variance (should be low since values are consistent)
long sum = 0, sumSq = 0;
for (var i = 0; i < 8; i++)
{
sum += ns._rttHistory[i];
sumSq += ns._rttHistory[i] * ns._rttHistory[i];
}
var mean = sum / 8;
ns._rttVariance = sumSq / 8 - mean * mean;
// Variance is low (consistent), but should NOT be "stable" due to high latency
Assert.False(ns.HasStableConnection,
$"400ms RTT should not be stable. AvgRTT={ns.AverageRtt}, Variance={ns._rttVariance}");
// Now verify detection gives more leniency for high-latency connections
SimulateMovements(ns,
(105, 100), (105, 100), (105, 100), (105, 100), (105, 100),
(105, 100), (105, 100), (105, 100), (105, 100), (105, 100)
);
var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out _, out var confidence);
// Slightly slow but high-latency player should get benefit of doubt
Assert.True(
verdict == MovementThrottle.DetectionVerdict.Normal ||
verdict == MovementThrottle.DetectionVerdict.Possible,
$"High latency player should get leniency. Verdict={verdict}"
);
}
/// <summary>
/// Scenario: RTT probing and measurement.
/// Verifies that RTT variance is calculated correctly and stability
/// is properly determined.
/// </summary>
[Fact]
public void RttMeasurement_VarianceCalculation()
{
var ns = PacketTestUtilities.CreateTestNetState();
SetTickCount(1000);
// Simulate stable low-latency RTT measurements
var rttValues = new long[] { 50, 48, 52, 49, 51, 50, 48, 52 };
ns._rttHistory = new long[8];
for (var i = 0; i < rttValues.Length; i++)
{
ns._rttHistory[i] = rttValues[i];
}
ns._rttSampleCount = 8;
// Calculate expected variance
long sum = 0, sumSq = 0;
foreach (var rtt in rttValues)
{
sum += rtt;
sumSq += rtt * rtt;
}
var expectedMean = sum / 8;
var expectedVariance = sumSq / 8 - expectedMean * expectedMean;
// Clamp negative variance (safety check)
if (expectedVariance < 0)
{
expectedVariance = 0;
}
ns._rttVariance = expectedVariance;
Assert.True(ns._rttVariance >= 0, "Variance should never be negative");
Assert.True(ns._rttVariance < 2500, "Low-variance samples should have variance < 2500");
Assert.True(ns.AverageRtt > 0 && ns.AverageRtt < 200, "Average RTT should be ~50ms");
Assert.True(ns.HasStableConnection, "Should be considered stable connection");
}
/// <summary>
/// Scenario: Actual speed hacker with stable connection.
/// Should be detected with high confidence.
/// </summary>
[Fact]
public void SpeedHack_StableConnection_DetectedDefinite()
{
var ns = PacketTestUtilities.CreateTestNetState();
// Set up stable, low-latency RTT
ns._rttHistory = new long[] { 30, 32, 28, 31, 29, 30, 31, 30 };
ns._rttSampleCount = 8;
long sum = 0, sumSq = 0;
for (var i = 0; i < 8; i++)
{
sum += ns._rttHistory[i];
sumSq += ns._rttHistory[i] * ns._rttHistory[i];
}
ns._rttVariance = sumSq / 8 - (sum / 8) * (sum / 8);
if (ns._rttVariance < 0)
{
ns._rttVariance = 0;
}
Assert.True(ns.HasStableConnection, "Should be stable for this test");
// Simulate speed hack: 50% faster movement
SimulateMovements(ns,
(67, 100), (67, 100), (67, 100), (67, 100), (67, 100),
(67, 100), (67, 100), (67, 100), (67, 100), (67, 100),
(67, 100), (67, 100), (67, 100), (67, 100), (67, 100)
);
var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out _, out var confidence);
Assert.True(rate > 1.4f, $"Expected rate > 1.4 (50% speed hack), got {rate}");
Assert.Equal(MovementThrottle.DetectionVerdict.Definite, verdict);
Assert.True(confidence > 0.5f, $"Expected high confidence, got {confidence}");
}
/// <summary>
/// Scenario: Borderline speed hack (7% faster) with unstable connection.
/// Should give benefit of doubt (not Definite).
/// </summary>
[Fact]
public void BorderlineSpeed_UnstableConnection_BenefitOfDoubt()
{
var ns = PacketTestUtilities.CreateTestNetState();
// Set up unstable connection
ns._rttHistory = new long[] { 50, 300, 80, 250, 100, 400, 60, 350 };
ns._rttSampleCount = 8;
long sum = 0, sumSq = 0;
for (var i = 0; i < 8; i++)
{
sum += ns._rttHistory[i];
sumSq += ns._rttHistory[i] * ns._rttHistory[i];
}
ns._rttVariance = sumSq / 8 - (sum / 8) * (sum / 8);
Assert.False(ns.HasStableConnection, "Should be unstable due to high variance");
// Simulate borderline speed (7% faster)
SimulateMovements(ns,
(93, 100), (93, 100), (93, 100), (93, 100), (93, 100),
(93, 100), (93, 100), (93, 100), (93, 100), (93, 100)
);
var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out _, out var confidence);
Assert.True(rate > 1.05f && rate < 1.10f, $"Expected rate ~1.07, got {rate}");
// Unstable connection + borderline rate = should NOT be Definite
Assert.True(
verdict != MovementThrottle.DetectionVerdict.Definite,
$"Borderline case with unstable connection should not be Definite. Verdict={verdict}, Confidence={confidence}"
);
}
/// <summary>
/// Scenario: Verifies that burst forgiveness triggers correctly and clears gap duration.
/// </summary>
[Fact]
public void MultipleLagSpikes_NoAccumulatedSuspicion()
{
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
@ -346,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
@ -358,73 +40,22 @@ 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}"
);
}
/// <summary>
/// Scenario: Walking vs running speed differences.
/// Walking (200ms interval) vs mounted running (100ms interval) should both
/// calculate correct rates.
/// </summary>
[Fact]
public void WalkingVsRunning_CorrectRateCalculation()
{
// Test walking (200ms expected interval)
var nsWalking = PacketTestUtilities.CreateTestNetState();
SimulateMovements(nsWalking,
(200, 200), (200, 200), (200, 200), (200, 200), (200, 200),
(200, 200), (200, 200), (200, 200), (200, 200), (200, 200)
);
var walkingRate = MovementThrottle.CalculateMovementRate(nsWalking, out var walkingSamples);
Assert.True(walkingSamples >= 8, "Should have enough walking samples");
Assert.True(walkingRate >= 0.95f && walkingRate <= 1.05f,
$"Walking rate should be ~1.0, got {walkingRate}");
// Test mounted running (100ms expected interval)
var nsRunning = PacketTestUtilities.CreateTestNetState();
SimulateMovements(nsRunning,
(100, 100), (100, 100), (100, 100), (100, 100), (100, 100),
(100, 100), (100, 100), (100, 100), (100, 100), (100, 100)
);
var runningRate = MovementThrottle.CalculateMovementRate(nsRunning, out var runningSamples);
Assert.True(runningSamples >= 8, "Should have enough running samples");
Assert.True(runningRate >= 0.95f && runningRate <= 1.05f,
$"Running rate should be ~1.0, got {runningRate}");
// Test speed hack while walking
var nsWalkingHack = PacketTestUtilities.CreateTestNetState();
SimulateMovements(nsWalkingHack,
(100, 200), (100, 200), (100, 200), (100, 200), (100, 200),
(100, 200), (100, 200), (100, 200), (100, 200), (100, 200)
);
var walkingHackRate = MovementThrottle.CalculateMovementRate(nsWalkingHack, out _);
Assert.True(walkingHackRate >= 1.9f && walkingHackRate <= 2.1f,
$"Walking at running speed should be rate ~2.0, got {walkingHackRate}");
}
}

View file

@ -212,30 +212,6 @@ public class MovementThrottleTests
Assert.True(rate > 1.25f, $"Expected rate > 1.25, got {rate}");
}
[Fact]
public void AnalyzeMovement_ModerateSpeedHack_ReturnsPossibleOrLikely()
{
// 8% faster than expected - moderate, should be Possible or Likely
var ns = CreateNetStateWithHistory(
(93, 100), (93, 100), (93, 100), (93, 100), (93, 100),
(93, 100), (93, 100), (93, 100), (93, 100), (93, 100)
);
// Set up stable, low-latency RTT (required for Likely verdict path)
ns._rttHistory = [50, 52, 48, 51, 49, 50, 51, 50];
ns._rttSampleCount = 8;
ns._rttVariance = 4; // Low variance
var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence);
Assert.True(
verdict == MovementThrottle.DetectionVerdict.Possible ||
verdict == MovementThrottle.DetectionVerdict.Likely,
$"Expected Possible or Likely, got {verdict}. Rate={rate}, Confidence={confidence}"
);
Assert.True(rate > 1.05f && rate < 1.15f, $"Expected rate ~1.07, got {rate}");
}
[Fact]
public void AnalyzeMovement_BurstAfterLargeGap_ReducesConfidence()
{
@ -279,25 +255,6 @@ public class MovementThrottleTests
Assert.Equal(MovementThrottle.DetectionVerdict.Definite, verdict);
}
[Fact]
public void AnalyzeMovement_StableConnectionLowLatency_IncreasesConfidence()
{
var ns = CreateNetStateWithHistory(
(95, 100), (95, 100), (95, 100), (95, 100), (95, 100),
(95, 100), (95, 100), (95, 100), (95, 100), (95, 100)
);
// Set up stable, low-latency RTT
ns._rttHistory = [50, 52, 48, 51, 49, 50, 51, 50];
ns._rttSampleCount = 8;
ns._rttVariance = 4; // Low variance
var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence);
// With stable connection, even small anomalies are more suspicious
Assert.True(confidence > 0.2f, $"Expected higher confidence for stable connection, got {confidence}");
}
[Fact]
public void AnalyzeMovement_UnstableConnection_ReducesConfidence()
{

View file

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

View file

@ -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<bool> 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
);

View file

@ -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<bool> 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,39 +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));
}
finally
{
ns.Dispose();
client.Close();
}
}
[Fact]
public void Send_WithPartialRemainder_GrowsAndCopiesFullPayload()
{
var ns = CreateAuthenticatedNetState(out var client);
var baseSize = ns._socket.SendBuffer.PhysicalSize;
try
{
var first = Pattern(baseSize / 2, 200);
var second = Pattern(baseSize / 2 + 1, 201);
var expected = new byte[first.Length + second.Length];
first.CopyTo(expected, 0);
second.CopyTo(expected, first.Length);
// Remainder too small for the second send
ns.Send(first);
ns.Send(second);
Assert.True(ns.Running);
Assert.Equal(string.Empty, ns._disconnectReason);
Assert.True(ns._socket.SendBuffer.PhysicalSize > baseSize);
Assert.True(ns._sendBufferGrown);
Assert.Equal(expected, ReadAll(client, expected.Length));
Assert.Equal(expected.ToArray(), NetTestHelpers.ReadAll(client, expected.Count));
}
finally
{
@ -433,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
@ -451,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
{
@ -471,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;
@ -503,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
@ -536,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);
@ -564,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);
@ -590,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);
@ -628,58 +528,4 @@ public class NetStateSendBufferTests
// growth off; budget untouched
Assert.Equal(1L, NetState.CoerceSendBufferGrowthBudget(1, sendBufferSize, sendBufferSize));
}
[Fact]
public void CoerceMaxBufferSlabs_ClampsToOneSlabPerConnection()
{
var maxConnections = NetState.SocketManager.MaxSockets;
// fewer than one slab is meaningless
Assert.Equal(1, NetState.CoerceMaxBufferSlabs(0));
Assert.Equal(1, NetState.CoerceMaxBufferSlabs(-4));
// more slabs than connections cannot make a slab any smaller
Assert.Equal(maxConnections, NetState.CoerceMaxBufferSlabs(maxConnections * 2));
Assert.Equal(128, NetState.CoerceMaxBufferSlabs(128));
}
[Fact]
public void CoerceInitialBufferSlabs_ClampsToTheSlabCount()
{
var slabs = NetState.BasePoolSlabCount(128);
Assert.True(slabs > 1);
Assert.Equal(1, NetState.CoerceInitialBufferSlabs(0, slabs));
Assert.Equal(1, NetState.CoerceInitialBufferSlabs(-1, slabs));
// a pool never holds more slabs than it has
Assert.Equal(slabs, NetState.CoerceInitialBufferSlabs(slabs + 1, slabs));
Assert.Equal(4, NetState.CoerceInitialBufferSlabs(4, slabs));
}
[Fact]
public void Ring_RegisteredBufferTable_MatchesTheConfiguredSlabCount()
{
var manager = NetState.SocketManager;
var maxBufferSlabs = NetState.CoerceMaxBufferSlabs(ServerConfiguration.GetSetting("network.maxBufferSlabs", NetState.DefaultMaxBufferSlabs));
// A table smaller than this throws at manager construction. Connections start on the
// transport minimum until the game server promotes them, so the formula must count those too.
// Mirrors what production passes: the request, not the effective size the manager may coerce
// down to (RequiredRegisteredBuffers treats a non-zero request as a pool either way).
Assert.Equal(
RingSocketManager.RequiredRegisteredBuffers(
manager.MaxSockets,
NetState.SendBufferSize,
manager.MaxSendBufferSize,
manager.SendBufferGrowthBudget,
maxBufferSlabs,
IORingBuffer.MinimumSize,
IORingBuffer.MinimumSize
),
NetState.Ring.MaxRegisteredBuffers
);
}
}

View file

@ -42,20 +42,6 @@ public class NetworkCompressionBoundsTests
}
}
[Fact]
public void StillCompressesWhenOutputIsLargeEnough()
{
var input = new byte[64];
Array.Fill(input, (byte)'A');
var output = new byte[256];
var written = NetworkCompression.Compress(input, output);
Assert.True(written > 0);
Assert.True(written <= output.Length);
}
[Fact]
public void ReportsFailureRatherThanOverrunningATightOutput()
{

View file

@ -1,80 +0,0 @@
using System.Net.Sockets;
using Server.Network;
using Xunit;
namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
public class NetworkStatsTests
{
private static byte[] Pattern(int length, int seed)
{
var data = new byte[length];
new System.Random(seed).NextBytes(data);
return data;
}
[Fact]
public void MaintainSendBuffers_RecordsTheRefusalsItResets()
{
var previous = NetState._availableMemoryBytes;
NetState ns = null;
Socket client = null;
try
{
ns = PacketTestUtilities.CreateTestNetState(out client);
ns._protocolState = NetState.ProtocolState.GameServer_AwaitingGameServerLogin;
ns.Account = new MockAccount();
var baseSize = ns._socket.SendBuffer.PhysicalSize;
NetState.MaintainSendBuffers(); // drain whatever earlier tests refused
NetState._availableMemoryBytes = 1; // any working set is above 80% of one byte
for (var i = 0; i < 6; i++)
{
ns.Send(Pattern(baseSize / 4, i));
}
// The sweep refreshes _availableMemoryBytes, so refusals must happen before it
NetState.MaintainSendBuffers();
var stats = NetState.GetNetworkStats();
Assert.True(stats.LastSweep.Ran);
Assert.True(stats.LastSweep.CeilingRefusals >= 1);
// Counters are per sweep, not cumulative
NetState.MaintainSendBuffers();
Assert.Equal(0, NetState.GetNetworkStats().LastSweep.CeilingRefusals);
}
finally
{
NetState._availableMemoryBytes = previous;
ns?.Dispose();
client?.Close();
}
}
[Fact]
public void GetNetworkStats_CountsAuthenticatedConnectionsSeparately()
{
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
var before = NetState.GetNetworkStats();
ns._protocolState = NetState.ProtocolState.GameServer_AwaitingGameServerLogin;
ns.Account = new MockAccount();
var after = NetState.GetNetworkStats();
Assert.Equal(before.Connected, after.Connected);
Assert.Equal(before.Authenticated + 1, after.Authenticated);
Assert.True(after.Connected >= after.Authenticated);
}
finally
{
ns.Dispose();
client.Close();
}
}
}

View file

@ -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<int, Mobile> _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));
}
}

View file

@ -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));
}
}

View file

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

View file

@ -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
);
}
}

View file

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

View file

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

View file

@ -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<EquipInfoAttribute>(info.Attributes)
PacketAssert.Sends(
new DisplayEquipmentInfo(item, info),
ns => ns.SendDisplayEquipmentInfo(
item.Serial,
info.Number,
info.Crafter?.RawName,
info.Unidentified,
new List<EquipInfoAttribute>(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));
}
}

View file

@ -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));
}
}

View file

@ -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, 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
);
}
}

View file

@ -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);
}
}

View file

@ -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)
);
}
}

View file

@ -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));
}
}

View file

@ -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
);
}
}

View file

@ -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);
}
}
}

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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)
);
}
}

View file

@ -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
);
}
}

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

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

View file

@ -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);
}
}

View file

@ -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<int>();
var withoutArgument = new HashSet<int>();
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]

View file

@ -25,18 +25,6 @@ public class ObjectPropertyListReentrancyTests
return value;
}
[Fact]
public void InterpolatedAdd_ResetMidHole_DoesNotThrow()
{
var opl = new ObjectPropertyList(null);
var ex = Record.Exception(
() => opl.Add(1060776, $"{ResettingHole(opl, "Knight")}\t{"Council of Mages"}")
);
Assert.Null(ex);
}
[Fact]
public void InterpolatedAdd_DisposeMidHole_DoesNotThrow()
{
@ -57,19 +45,6 @@ public class ObjectPropertyListReentrancyTests
[Collection("Sequential Server Tests")]
public class ObjectPropertyListNestedBuildTests
{
[Fact]
public void NestedBuild_DoesNotUnguardTheOuterList()
{
var outer = new ObjectPropertyList(null);
var inner = new ObjectPropertyList(null);
outer.IsBuilding = true;
inner.IsBuilding = true; // another entity starts building, and finishes
inner.IsBuilding = false;
Assert.True(outer.IsBuilding);
}
[Fact]
public void Reset_MidInterpolation_LeavesTheListUsable()
{

View file

@ -82,4 +82,112 @@ public class AnchoredItemSerializationTests
restored?.Delete();
}
}
/// <summary>
/// LastMoved is the real time of the move, so a load after downtime must not shift it.
/// Shifting it rewrote every item record on the first save after every restart.
/// </summary>
[Fact]
public void LastMoved_IsNotShiftedByTheLoadAnchor()
{
var item = new Item(0x1F13);
var moved = Core.Now - TimeSpan.FromDays(30);
item.LastMoved = moved;
var restored = new Item((Serial)0x7ffff124u);
restored.Deserialize(new BufferReader(SerializeItem(item)) { AnchoredTimeShift = TimeSpan.FromHours(2) });
Assert.Equal(moved, restored.LastMoved);
Assert.Equal(default(DateTime), restored.DecayResetTime);
item.Delete();
restored.Delete();
}
/// <summary>
/// Downtime still does not count toward decay: a countdown running when the save started
/// resumes with the time it had left, carried by DecayResetTime.
/// </summary>
[Fact]
public void Downtime_IsCreditedToARunningDecayCountdown()
{
var start = Core._now;
try
{
var item = new Item(0x1F13);
item.LastMoved = start - TimeSpan.FromMinutes(10);
var deadline = item.ScheduledDecayTime;
var downtime = TimeSpan.FromHours(2);
Core._now = start + downtime;
item.CreditDowntimeToDecay(start, downtime);
Assert.Equal(start - TimeSpan.FromMinutes(10), item.LastMoved);
Assert.Equal(deadline + downtime, item.ScheduledDecayTime);
item.Delete();
}
finally
{
Core._now = start;
}
}
/// <summary>
/// The common case: an item whose countdown had already run out gets no credit, so a restart
/// leaves its record byte-identical.
/// </summary>
[Fact]
public void ItemAlreadyDueAtSave_GetsNoCredit_AndKeepsItsBytes()
{
var start = Core._now;
try
{
var item = new Item(0x1F13);
item.LastMoved = start - TimeSpan.FromDays(3);
var before = SerializeItem(item);
var downtime = TimeSpan.FromMinutes(30);
Core._now = start + downtime;
item.CreditDowntimeToDecay(start, downtime);
Assert.Equal(default(DateTime), item.DecayResetTime);
Assert.Equal(before, SerializeItem(item));
item.Delete();
}
finally
{
Core._now = start;
}
}
/// <summary>
/// Stat-gain stamps are absolute and not shifted at load either; downtime counts toward a
/// cooldown that is only minutes long.
/// </summary>
[Fact]
public void StatGainStamps_AreNotShiftedByTheLoadAnchor()
{
var mobile = new Mobile(World.NewMobile);
mobile.DefaultMobileInit();
mobile.LastStrGain = Core.Now - TimeSpan.FromMinutes(3);
mobile.LastIntGain = Core.Now - TimeSpan.FromMinutes(2);
mobile.LastDexGain = Core.Now - TimeSpan.FromMinutes(1);
var writer = new BufferWriter(new byte[4096], true);
mobile.Serialize(writer);
var restored = new Mobile(World.NewMobile);
restored.Deserialize(new BufferReader(writer.Buffer[..(int)writer.Position]) { AnchoredTimeShift = TimeSpan.FromHours(2) });
Assert.Equal(mobile.LastStrGain, restored.LastStrGain);
Assert.Equal(mobile.LastIntGain, restored.LastIntGain);
Assert.Equal(mobile.LastDexGain, restored.LastDexGain);
mobile.Delete();
restored.Delete();
}
}

View file

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using Xunit;
namespace Server.Tests;
@ -69,6 +67,63 @@ public class AnchoredTimeTests
Assert.Equal(local.ToUniversalTime(), read(TimeSpan.Zero).ReadAnchoredTime());
}
private static readonly DateTime SaveStart = new(2026, 8, 22, 12, 0, 0, DateTimeKind.Utc);
private static IGenericReader ReadSaved(BufferWriter writer, TimeSpan downtime) =>
new BufferReader(writer.Buffer) { AnchoredTimeShift = downtime, AnchoredTimeOrigin = SaveStart };
[Fact]
public void AnchoredTime_DeadlineRunningAtSaveStart_KeepsItsRemainingTime()
{
var writer = new BufferWriter(new byte[64], true);
var deadline = SaveStart + TimeSpan.FromMinutes(20);
var downtime = TimeSpan.FromHours(3);
writer.WriteAnchoredTime(deadline);
Assert.Equal(deadline + downtime, ReadSaved(writer, downtime).ReadAnchoredTime());
}
/// <summary>
/// A deadline that had passed before the save must stay passed. Shifting it brought an expiry
/// back into the future whenever the downtime was longer than the time since it expired.
/// </summary>
[Fact]
public void AnchoredTime_DeadlinePassedBeforeSaveStart_StaysPassed()
{
var writer = new BufferWriter(new byte[64], true);
var expired = SaveStart - TimeSpan.FromMinutes(5);
writer.WriteAnchoredTime(expired);
Assert.Equal(expired, ReadSaved(writer, TimeSpan.FromMinutes(30)).ReadAnchoredTime());
}
/// <summary>
/// A stamp of something that already happened (a last-use time) is never shifted, so its
/// bytes survive a restart and downtime counts toward its window.
/// </summary>
[Fact]
public void AnchoredTime_PastStamp_IsNotShifted()
{
var writer = new BufferWriter(new byte[64], true);
var lastUse = SaveStart - TimeSpan.FromDays(2);
writer.WriteAnchoredTime(lastUse);
Assert.Equal(lastUse, ReadSaved(writer, TimeSpan.FromHours(1)).ReadAnchoredTime());
}
[Fact]
public void AnchoredTime_ValueAtSaveStart_IsNotShifted()
{
var writer = new BufferWriter(new byte[64], true);
writer.WriteAnchoredTime(SaveStart);
Assert.Equal(SaveStart, ReadSaved(writer, TimeSpan.FromHours(1)).ReadAnchoredTime());
}
}
internal class AnchoredEntity : ISerializable
@ -107,83 +162,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}."
);
}
}

View file

@ -1,46 +0,0 @@
using System;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using Xunit;
namespace Server.Tests;
public class DecimalSerializationTests
{
private static void Write(byte[] buffer, decimal value)
{
Span<int> bytes = stackalloc int[sizeof(decimal) / 4];
decimal.GetBits(value, bytes);
MemoryMarshal.Cast<int, byte>(bytes).CopyTo(buffer.AsSpan());
}
private static int ReadInt(ReadOnlySpan<byte> buffer) => BinaryPrimitives.ReadInt32LittleEndian(buffer);
private static decimal ReadDecimal(ReadOnlySpan<byte> buffer) => new(stackalloc int[4] { ReadInt(buffer), ReadInt(buffer[4..]), ReadInt(buffer[8..]), ReadInt(buffer[12..]) });
public static TheoryData<decimal> Data =>
new()
{
123.46m,
0.0256m,
10000m,
2m,
0.0001m,
0.0000000000000000000000000001m
};
[Theory]
[MemberData(nameof(Data))]
public void TestSerializeDecimal(decimal value)
{
// Arrange
var buffer = new byte[sizeof(decimal)];
// Act
Write(buffer, value);
// Assert
Assert.Equal(value, ReadDecimal(buffer));
}
}

View file

@ -1,21 +0,0 @@
using System;
using Xunit;
namespace Server.Tests;
public class EnumConversionTests
{
[Fact]
public void TestToEnum()
{
var e = ReadEnum<TileFlag>();
Assert.Equal(TileFlag.Container, e);
}
private static unsafe T ReadEnum<T>() where T : unmanaged, Enum
{
var num = (long)TileFlag.Container;
return *(T*)&num;
}
}

View file

@ -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 _));
}
/// <summary>
@ -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<InvalidOperationException>(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<InvalidOperationException>(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<InvalidOperationException>(() => persistence.WriteSnapshot(dir));
persistence.PostWorldSave();
}
finally
{
persistence.Unregister();
foreach (var worker in workers)
{
worker.Exit();
}
World._threadWorkers = previousWorkers;
Directory.Delete(dir, true);
}
Assert.Throws<InvalidOperationException>(() => persistence.WriteSnapshot(pipeline.Directory));
persistence.PostWorldSave();
}
}

View file

@ -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<TestEntity>();
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<TestEntity>();
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<TestEntity>();
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<TestEntity>();
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);
}
}

View file

@ -0,0 +1,59 @@
using System.IO;
using Xunit;
namespace Server.Tests.Terrain;
[Collection("Sequential Server Tests")]
public class SyntheticTileDataTests
{
[Fact]
public void GeneratedFile_IsPreHsSize()
{
Assert.Equal(1_036_288, SyntheticTileData.Build().Length);
}
// Inside the test host's output, rewritten every run, so runs leave nothing behind in the temp directory.
[Fact]
public void DataDirectory_IsTheOnlyDataDirectory_AndLivesInTheTestHost()
{
Assert.Equal(Path.Combine(Core.BaseDirectory, "SyntheticData"), SyntheticWorld.DataDirectory);
Assert.Equal([SyntheticWorld.DataDirectory], ServerConfiguration.DataDirectories);
}
[Fact]
public void KitIds_FitThePreHsTable()
{
for (var i = 0; i < TestTiles.Items.Count; i++)
{
Assert.True(TestTiles.Items[i].Id < 0x4000, $"{TestTiles.Items[i].Name} is outside the pre-HS item table");
}
for (var i = 0; i < TestTiles.Land.Count; i++)
{
Assert.True(TestTiles.Land[i].Id < 0x4000, $"{TestTiles.Land[i].Name} is outside the land table");
}
}
[Fact]
public void GeneratedFile_RoundTripsKitThroughLoad()
{
SyntheticTileData.LoadInto(SyntheticWorld.DataDirectory);
for (var i = 0; i < TestTiles.Items.Count; i++)
{
var tile = TestTiles.Items[i];
Assert.Equal(tile.Flags, TileData.ItemTable[tile.Id].Flags);
Assert.Equal((int)tile.Height, TileData.ItemTable[tile.Id].Height);
Assert.Equal(tile.Name, TileData.ItemTable[tile.Id].Name);
}
for (var i = 0; i < TestTiles.Land.Count; i++)
{
var tile = TestTiles.Land[i];
Assert.Equal(tile.Flags, TileData.LandTable[tile.Id].Flags);
}
Assert.Equal(TileFlag.None, TileData.ItemTable[0x2A0].Flags);
Assert.Equal(TileFlag.None, TileData.LandTable[3].Flags);
}
}

View file

@ -0,0 +1,33 @@
using Xunit;
namespace Server.Tests.Terrain;
[Collection("Sequential Server Tests")]
public class TestMultisTests
{
[Theory]
[InlineData(TestMultis.GuildHouse)]
[InlineData(TestMultis.Tower)]
[InlineData(TestMultis.SmallBoatNorth)]
public void Seed_RegistersFootprint(int multiID)
{
Assert.NotEmpty(MultiData.GetComponents(multiID).List);
}
[Fact]
public void Footprints_DifferBetweenGuildHouseAndTower()
{
var house = MultiData.GetComponents(TestMultis.GuildHouse);
var tower = MultiData.GetComponents(TestMultis.Tower);
Assert.NotEqual(house.Width, tower.Width);
}
[Fact]
public void GuildHouse_HasAnOpenDoorway()
{
var house = MultiData.GetComponents(TestMultis.GuildHouse);
var doorway = house.Tiles[house.Center.X][house.Height - 1];
Assert.Single(doorway);
Assert.Equal(TestTiles.Floor, doorway[0].ID & TileData.MaxItemValue);
}
}

View file

@ -0,0 +1,125 @@
using System;
using Xunit;
namespace Server.Tests.Terrain;
[Collection("Sequential Server Tests")]
public class TestTerrainTests
{
// Default ground outside the prefab region.
private const int X = 1400;
private const int Y = 1400;
[Fact]
public void StampingOneTile_KeepsTheRestOfTheBlock()
{
using var scope = TestTerrain.Scope();
TestTerrain.SetLand(X, Y, TestTiles.Grass, 3);
TestTerrain.SetLand(X + 1, Y, TestTiles.Dirt, 4);
Assert.Equal(TestTiles.Grass, Map.Felucca.Tiles.GetLandTile(X, Y).ID);
Assert.Equal(TestTiles.Dirt, Map.Felucca.Tiles.GetLandTile(X + 1, Y).ID);
}
[Fact]
public void StampingAnUninjectedBlock_DoesNotTouchOtherBlocks()
{
using var scope = TestTerrain.Scope();
TestTerrain.SetLand(X, Y, TestTiles.Grass, 3);
Assert.Equal(0, Map.Felucca.Tiles.GetLandTile(X + 64, Y + 64).ID);
Assert.Equal(0, Map.Felucca.Tiles.GetLandTile(X + 1, Y).ID);
}
[Fact]
public void Stamps_ReachBothFacets()
{
using var scope = TestTerrain.Scope();
TestTerrain.SetLand(X, Y, TestTiles.Grass, 3);
TestTerrain.AddStatic(X, Y, TestTiles.Wall, 3);
foreach (var map in (ReadOnlySpan<Map>)[Map.Felucca, Map.Trammel])
{
Assert.Equal(TestTiles.Grass, map.Tiles.GetLandTile(X, Y).ID);
var statics = map.Tiles.GetStaticBlock(X >> 3, Y >> 3)[X & 7][Y & 7];
Assert.Single(statics);
Assert.Equal(TestTiles.Wall, statics[0].ID);
}
}
[Fact]
public void ScopeDispose_RestoresBothFacets()
{
using (TestTerrain.Scope())
{
TestTerrain.SetLand(X, Y, TestTiles.Grass, 3);
TestTerrain.AddStatic(X, Y, TestTiles.Wall, 3);
}
foreach (var map in (ReadOnlySpan<Map>)[Map.Felucca, Map.Trammel])
{
Assert.Equal(0, map.Tiles.GetLandTile(X, Y).ID);
Assert.Empty(map.Tiles.GetStaticBlock(X >> 3, Y >> 3)[X & 7][Y & 7]);
}
}
[Fact]
public void ScopeDispose_RestoresAPrefabItOverwrote()
{
Prefabs.StampAll();
var coast = Prefabs.Coastline.Anchor;
var before = Map.Felucca.Tiles.GetLandTile(coast.X, coast.Y);
using (TestTerrain.Scope())
{
TestTerrain.SetLand(coast.X, coast.Y, TestTiles.RockLand, 40);
}
Assert.Equal(before.ID, Map.Felucca.Tiles.GetLandTile(coast.X, coast.Y).ID);
Assert.Equal(before.Z, Map.Felucca.Tiles.GetLandTile(coast.X, coast.Y).Z);
Prefabs.Coastline.AssertPresent();
}
[Fact]
public void NestedScopes_Throw()
{
using var scope = TestTerrain.Scope();
var error = Assert.Throws<InvalidOperationException>(() => TestTerrain.Scope());
Assert.Contains($"{nameof(TestTerrainTests)}.{nameof(NestedScopes_Throw)}", error.Message);
}
[Fact]
public void StampingOutsideAScope_Throws_OncePrefabsAreStamped()
{
Prefabs.StampAll();
Assert.Throws<InvalidOperationException>(() => TestTerrain.SetLand(X, Y, TestTiles.Grass, 0));
}
[Fact]
public void StampAll_PrefabsDoNotOverlap_AndAreAllPresent()
{
Prefabs.StampAll();
for (var i = 0; i < Prefabs.All.Count; i++)
{
Prefabs.All[i].AssertPresent();
}
}
[Fact]
public void AssertPresent_Throws_WhenThePrefabIsGone()
{
Prefabs.StampAll();
var coast = Prefabs.Coastline.Anchor;
using var scope = TestTerrain.Scope();
TestTerrain.SetLand(coast.X, coast.Y, 0, 0);
Assert.Throws<InvalidOperationException>(() => Prefabs.Coastline.AssertPresent());
}
}

View file

@ -85,6 +85,30 @@ public class TimerTests
Assert.Equal(2, timerTicks.ExecutedCount);
}
[Fact]
public void ClearAllTimers_StopsEveryTimerInASlot()
{
Timer.Init(0);
var timers = new Timer[3];
for (var i = 0; i < timers.Length; i++)
{
timers[i] = Timer.DelayCall(TimeSpan.FromMilliseconds(80), () => Assert.Fail("cleared timer fired"));
}
Timer.ClearAllTimers(0);
for (var i = 0; i < timers.Length; i++)
{
Assert.False(timers[i].Running);
}
for (var tick = 8; tick <= 160; tick += 8)
{
Timer.Slice(tick);
}
}
private class TimerTicks
{
public long ExpectedTicks;

View file

@ -54,15 +54,6 @@ namespace Server.Tests
Assert.Equal("Test ModernUO", result.Name);
}
[Fact]
public void TestTwoParamsOptionalActivator()
{
var result = typeof(TestTwoOptionalParamsClass).CreateInstance<TestTwoOptionalParamsClass>(10, "Prod ModernUO");
Assert.IsType<TestTwoOptionalParamsClass>(result);
Assert.Equal(10, result.Amount);
Assert.Equal("Prod ModernUO", result.Name);
}
private class TestZeroParamsClass
{
}

View file

@ -35,54 +35,6 @@ public class HtmlEscapeTests
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Multiple instances of same character")]
[InlineData("<><>", "&lt;&gt;&lt;&gt;")]
[InlineData("&&&&", "&amp;&amp;&amp;&amp;")]
[InlineData("\"\"\"", "&quot;&quot;&quot;")]
[InlineData("'''", "&#39;&#39;&#39;")]
[InlineData(">>>", "&gt;&gt;&gt;")]
public void EscapeHtml_MultipleSpecialCharacters_EscapesAll(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Mixed content")]
[InlineData("Hello <world>", "Hello &lt;world&gt;")]
[InlineData("<div>Hello</div>", "&lt;div&gt;Hello&lt;/div&gt;")]
[InlineData("Tom & Jerry", "Tom &amp; Jerry")]
[InlineData("He said \"hello\"", "He said &quot;hello&quot;")]
[InlineData("It's a test", "It&#39;s a test")]
public void EscapeHtml_MixedContent_EscapesMixedSpecialCharacters(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Starting with special character")]
[InlineData("<Hello", "&lt;Hello")]
[InlineData(">World", "&gt;World")]
[InlineData("&Start", "&amp;Start")]
[InlineData("\"Quote", "&quot;Quote")]
[InlineData("'Apostrophe", "&#39;Apostrophe")]
public void EscapeHtml_StartsWithSpecialCharacter_EscapesStart(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Ending with special character")]
[InlineData("Hello<", "Hello&lt;")]
[InlineData("World>", "World&gt;")]
[InlineData("End&", "End&amp;")]
[InlineData("Quote\"", "Quote&quot;")]
[InlineData("Test'", "Test&#39;")]
public void EscapeHtml_EndsWithSpecialCharacter_EscapesEnd(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Complex mixed scenarios")]
[InlineData("<p>Hello & goodbye</p>", "&lt;p&gt;Hello &amp; goodbye&lt;/p&gt;")]
[InlineData("&lt;already&gt;", "&amp;lt;already&amp;gt;")]
@ -103,59 +55,6 @@ public class HtmlEscapeTests
Assert.Empty(result);
}
[Fact(DisplayName = "Empty string input")]
public void EscapeHtml_EmptyString_ReturnsEmpty()
{
var result = "".EscapeHtml();
Assert.Empty(result);
}
[Fact(DisplayName = "Only special characters")]
public void EscapeHtml_OnlySpecialCharacters_EscapesAll()
{
const string input = "<>&\"'";
const string expected = "&lt;&gt;&amp;&quot;&#39;";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "Ampersand must be escaped first")]
public void EscapeHtml_AmpersandFirst_PreventDoubleEscaping()
{
// This is critical: & must be escaped to &amp;
// If we're not careful, we could double-escape already-escaped content
const string input = "&lt;";
const string expected = "&amp;lt;";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "ReadOnlySpan overload - no special characters")]
[InlineData("Hello World")]
[InlineData("Plain text")]
public void EscapeHtml_ReadOnlySpan_NoSpecialCharacters_ReturnsUnchanged(string input)
{
var result = input.AsSpan().EscapeHtml();
Assert.Equal(input, result);
}
[Theory(DisplayName = "ReadOnlySpan overload - with special characters")]
[InlineData("<div>", "&lt;div&gt;")]
[InlineData("Tom & Jerry", "Tom &amp; Jerry")]
public void EscapeHtml_ReadOnlySpan_WithSpecialCharacters_EscapesCorrectly(string input, string expected)
{
var result = input.AsSpan().EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "ReadOnlySpan overload - empty input")]
[InlineData("")]
public void EscapeHtml_ReadOnlySpan_Empty_ReturnsEmpty(string input)
{
var result = input.AsSpan().EscapeHtml();
Assert.Empty(result);
}
[Fact(DisplayName = "Consecutive special characters")]
public void EscapeHtml_ConsecutiveSpecialCharacters_EscapesAll()
{
@ -165,51 +64,6 @@ public class HtmlEscapeTests
Assert.Equal(expected, result);
}
[Fact(DisplayName = "Special characters with single normal character between")]
public void EscapeHtml_SpecialCharactersWithGaps_EscapesAll()
{
const string input = "<a>b&c\"d'e";
const string expected = "&lt;a&gt;b&amp;c&quot;d&#39;e";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "HTML tags")]
public void EscapeHtml_HtmlTags_EscapesTagBrackets()
{
var input = "<html><body>Hello</body></html>";
var expected = "&lt;html&gt;&lt;body&gt;Hello&lt;/body&gt;&lt;/html&gt;";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "HTML attributes with mixed quotes")]
public void EscapeHtml_HtmlAttributesWithQuotes_EscapesCorrectly()
{
var input = "<a href=\"test\" data='value'>";
var expected = "&lt;a href=&quot;test&quot; data=&#39;value&#39;&gt;";
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Theory(DisplayName = "Whitespace handling")]
[InlineData(" spaces ", " spaces ")]
[InlineData("\ttabs\t", "\ttabs\t")]
[InlineData("\nnewlines\n", "\nnewlines\n")]
public void EscapeHtml_Whitespace_PreservedAsIs(string input, string expected)
{
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "Performance: long string without special characters")]
public void EscapeHtml_LongStringNoSpecialCharacters_ReturnsQuickly()
{
var input = new string('a', 10000);
var result = input.EscapeHtml();
Assert.Equal(input, result);
}
[Fact(DisplayName = "Performance: long string with special characters")]
public void EscapeHtml_LongStringWithSpecialCharacters_HandlesCorrectly()
{
@ -229,15 +83,4 @@ public class HtmlEscapeTests
var result = input.EscapeHtml();
Assert.Equal(expected, result);
}
[Fact(DisplayName = "String overload matches ReadOnlySpan overload")]
public void EscapeHtml_StringVsReadOnlySpan_ProduceSameResult()
{
const string input = "<div>Tom & Jerry 'in' \"quotes\"</div>";
var resultString = input.EscapeHtml();
var resultSpan = input.AsSpan().EscapeHtml();
Assert.Equal(resultString, resultSpan);
}
}

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