#W# Merge: Merged with upstream. Conflicts resolved.
This commit is contained in:
commit
f6f50470a9
276 changed files with 6742 additions and 8845 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
33
Projects/Server.Tests/Helpers/EntityCleanup.cs
Normal file
33
Projects/Server.Tests/Helpers/EntityCleanup.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
152
Projects/Server.Tests/Helpers/NetTestHelpers.cs
Normal file
152
Projects/Server.Tests/Helpers/NetTestHelpers.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Projects/Server.Tests/Helpers/PacketAssert.cs
Normal file
31
Projects/Server.Tests/Helpers/PacketAssert.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
130
Projects/Server.Tests/Helpers/PersistencePipelineScope.cs
Normal file
130
Projects/Server.Tests/Helpers/PersistencePipelineScope.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Projects/Server.Tests/Helpers/SimulatedClockScope.cs
Normal file
42
Projects/Server.Tests/Helpers/SimulatedClockScope.cs
Normal 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;
|
||||
}
|
||||
82
Projects/Server.Tests/Helpers/TestClientHarness.cs
Normal file
82
Projects/Server.Tests/Helpers/TestClientHarness.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
17
Projects/Server.Tests/Helpers/TestMulti.cs
Normal file
17
Projects/Server.Tests/Helpers/TestMulti.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
306
Projects/Server.Tests/Terrain/Prefabs.cs
Normal file
306
Projects/Server.Tests/Terrain/Prefabs.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
108
Projects/Server.Tests/Terrain/SyntheticTileData.cs
Normal file
108
Projects/Server.Tests/Terrain/SyntheticTileData.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
27
Projects/Server.Tests/Terrain/SyntheticWorld.cs
Normal file
27
Projects/Server.Tests/Terrain/SyntheticWorld.cs
Normal 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();
|
||||
}
|
||||
63
Projects/Server.Tests/Terrain/TestMultis.cs
Normal file
63
Projects/Server.Tests/Terrain/TestMultis.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
215
Projects/Server.Tests/Terrain/TestTerrain.cs
Normal file
215
Projects/Server.Tests/Terrain/TestTerrain.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
61
Projects/Server.Tests/Terrain/TestTiles.cs
Normal file
61
Projects/Server.Tests/Terrain/TestTiles.cs
Normal 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)
|
||||
];
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>))]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
79
Projects/Server.Tests/Tests/Maps/TileMatrixInjectionTests.cs
Normal file
79
Projects/Server.Tests/Tests/Maps/TileMatrixInjectionTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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*)#
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
33
Projects/Server.Tests/Tests/Terrain/TestMultisTests.cs
Normal file
33
Projects/Server.Tests/Tests/Terrain/TestMultisTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
125
Projects/Server.Tests/Tests/Terrain/TestTerrainTests.cs
Normal file
125
Projects/Server.Tests/Tests/Terrain/TestTerrainTests.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,54 +35,6 @@ public class HtmlEscapeTests
|
|||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Multiple instances of same character")]
|
||||
[InlineData("<><>", "<><>")]
|
||||
[InlineData("&&&&", "&&&&")]
|
||||
[InlineData("\"\"\"", """"")]
|
||||
[InlineData("'''", "'''")]
|
||||
[InlineData(">>>", ">>>")]
|
||||
public void EscapeHtml_MultipleSpecialCharacters_EscapesAll(string input, string expected)
|
||||
{
|
||||
var result = input.EscapeHtml();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Mixed content")]
|
||||
[InlineData("Hello <world>", "Hello <world>")]
|
||||
[InlineData("<div>Hello</div>", "<div>Hello</div>")]
|
||||
[InlineData("Tom & Jerry", "Tom & Jerry")]
|
||||
[InlineData("He said \"hello\"", "He said "hello"")]
|
||||
[InlineData("It's a test", "It's a test")]
|
||||
public void EscapeHtml_MixedContent_EscapesMixedSpecialCharacters(string input, string expected)
|
||||
{
|
||||
var result = input.EscapeHtml();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Starting with special character")]
|
||||
[InlineData("<Hello", "<Hello")]
|
||||
[InlineData(">World", ">World")]
|
||||
[InlineData("&Start", "&Start")]
|
||||
[InlineData("\"Quote", ""Quote")]
|
||||
[InlineData("'Apostrophe", "'Apostrophe")]
|
||||
public void EscapeHtml_StartsWithSpecialCharacter_EscapesStart(string input, string expected)
|
||||
{
|
||||
var result = input.EscapeHtml();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Ending with special character")]
|
||||
[InlineData("Hello<", "Hello<")]
|
||||
[InlineData("World>", "World>")]
|
||||
[InlineData("End&", "End&")]
|
||||
[InlineData("Quote\"", "Quote"")]
|
||||
[InlineData("Test'", "Test'")]
|
||||
public void EscapeHtml_EndsWithSpecialCharacter_EscapesEnd(string input, string expected)
|
||||
{
|
||||
var result = input.EscapeHtml();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Complex mixed scenarios")]
|
||||
[InlineData("<p>Hello & goodbye</p>", "<p>Hello & goodbye</p>")]
|
||||
[InlineData("<already>", "&lt;already&gt;")]
|
||||
|
|
@ -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 = "<>&"'";
|
||||
var result = input.EscapeHtml();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Ampersand must be escaped first")]
|
||||
public void EscapeHtml_AmpersandFirst_PreventDoubleEscaping()
|
||||
{
|
||||
// This is critical: & must be escaped to &
|
||||
// If we're not careful, we could double-escape already-escaped content
|
||||
const string input = "<";
|
||||
const string expected = "&lt;";
|
||||
var result = input.EscapeHtml();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "ReadOnlySpan overload - no special characters")]
|
||||
[InlineData("Hello World")]
|
||||
[InlineData("Plain text")]
|
||||
public void EscapeHtml_ReadOnlySpan_NoSpecialCharacters_ReturnsUnchanged(string input)
|
||||
{
|
||||
var result = input.AsSpan().EscapeHtml();
|
||||
Assert.Equal(input, result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "ReadOnlySpan overload - with special characters")]
|
||||
[InlineData("<div>", "<div>")]
|
||||
[InlineData("Tom & Jerry", "Tom & Jerry")]
|
||||
public void EscapeHtml_ReadOnlySpan_WithSpecialCharacters_EscapesCorrectly(string input, string expected)
|
||||
{
|
||||
var result = input.AsSpan().EscapeHtml();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "ReadOnlySpan overload - empty input")]
|
||||
[InlineData("")]
|
||||
public void EscapeHtml_ReadOnlySpan_Empty_ReturnsEmpty(string input)
|
||||
{
|
||||
var result = input.AsSpan().EscapeHtml();
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Consecutive special characters")]
|
||||
public void EscapeHtml_ConsecutiveSpecialCharacters_EscapesAll()
|
||||
{
|
||||
|
|
@ -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 = "<a>b&c"d'e";
|
||||
var result = input.EscapeHtml();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "HTML tags")]
|
||||
public void EscapeHtml_HtmlTags_EscapesTagBrackets()
|
||||
{
|
||||
var input = "<html><body>Hello</body></html>";
|
||||
var expected = "<html><body>Hello</body></html>";
|
||||
var result = input.EscapeHtml();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "HTML attributes with mixed quotes")]
|
||||
public void EscapeHtml_HtmlAttributesWithQuotes_EscapesCorrectly()
|
||||
{
|
||||
var input = "<a href=\"test\" data='value'>";
|
||||
var expected = "<a href="test" data='value'>";
|
||||
var result = input.EscapeHtml();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Whitespace handling")]
|
||||
[InlineData(" spaces ", " spaces ")]
|
||||
[InlineData("\ttabs\t", "\ttabs\t")]
|
||||
[InlineData("\nnewlines\n", "\nnewlines\n")]
|
||||
public void EscapeHtml_Whitespace_PreservedAsIs(string input, string expected)
|
||||
{
|
||||
var result = input.EscapeHtml();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Performance: long string without special characters")]
|
||||
public void EscapeHtml_LongStringNoSpecialCharacters_ReturnsQuickly()
|
||||
{
|
||||
var input = new string('a', 10000);
|
||||
var result = input.EscapeHtml();
|
||||
Assert.Equal(input, result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Performance: long string with special characters")]
|
||||
public void EscapeHtml_LongStringWithSpecialCharacters_HandlesCorrectly()
|
||||
{
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue