test: remove trivial, duplicate and content-value tests (#2721)
## Summary Many tests proved a specific change when it landed but don't need to run on every CI build forever. This removes **202 test methods and 9 files: 313 test cases, 2,247 → 1,934**. UOContent.Tests drops from about 14s to about 5s locally. Removed: - **Tautologies.** Asserting a constant, a constructor readback or a trivial getter, or testing BCL behavior. - **Exact duplicates** of a surviving test on the same code path. - **Obsolete regressions** whose failure can no longer be expressed. - **Game-content values** shard owners customize (era drop rates, loot and skill tables, single-creature tuning). Each customization would otherwise force a test fix. ## How it was decided 1. Every test was classified, with a reason, by a per-shard sweep over all 287 test files. 2. Each proposed deletion was then checked by a second, adversarial reviewer against the surviving tests and the production code. 3. Anything that was the only coverage of a separate code path was **kept**, even where it looked like a duplicate. Examples: the unsigned `SpanReader`/`SpanWriter` methods, the at-location versus bounds enumerators, null-map enumerables, the security filters, archive-journal state transitions. About 130 proposed deletions were rejected this way. Whole files removed: - `FishingTests`, apart from 3 tests: the map-edge scan, serpent cleanup, and SOS chest consumption. The SOS test now stops at the first chest instead of measuring a catch rate. The multi-million-cast rate simulations are gone. - `LordOaksTests` and `MeerEnragedTests` (single-creature tests). - `AutoArchiveHelperTests`, `JsonDiscoverableTypeAttributeTests`, `DecimalSerializationTests` and `EnumConversionTests` (tautologies, or tests of their own local helpers). - `NetworkStatsTests`. - `MigratedDataLoadTests` (a subset of `AllSpawnFilesLoadTests`). - `SpawnerFixtureCapture`: a capture tool, not a test, and its `version == 12` assert can no longer pass. The `.bin` fixtures it produced stay. ## Tests - Server.Tests: 811 pass. - UOContent.Tests: 1,123 pass. A follow-up PR, stacked on this one, fixes how the remaining tests behave: shared state, CI-vacuous tests, harness duplication.
This commit is contained in:
parent
078be52777
commit
9a25054809
89 changed files with 11 additions and 4603 deletions
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -21,32 +21,6 @@ public class DecayRegistrationTests
|
|||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
|
|
@ -103,68 +77,9 @@ public class DecayRegistrationTests
|
|||
}
|
||||
}
|
||||
|
||||
[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.");
|
||||
|
||||
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()
|
||||
|
|
@ -475,24 +390,4 @@ public class DecayRegistrationTests
|
|||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,39 +220,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
|
||||
|
|
|
|||
|
|
@ -23,32 +23,6 @@ public class CanSpawnMobileTileDataTests
|
|||
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]
|
||||
public void CanSpawnMobile_FindsLowestValidSurface()
|
||||
{
|
||||
|
|
@ -239,51 +213,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));
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,29 +7,6 @@ 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()
|
||||
{
|
||||
|
|
@ -144,24 +121,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 +211,6 @@ public class StaticTileEnumeratorTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticTileEnumerator_EmptyReturnsCorrectly()
|
||||
{
|
||||
var enumerator = Map.StaticTileEnumerable.Empty.GetEnumerator();
|
||||
Assert.False(enumerator.MoveNext());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticTileEnumerator_DeletedMultiSkipped()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -74,254 +74,6 @@ public class MovementThrottleLatencyTests
|
|||
}
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
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>
|
||||
|
|
@ -383,48 +135,4 @@ public class MovementThrottleLatencyTests
|
|||
$"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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -390,38 +390,6 @@ public class NetStateSendBufferTests
|
|||
}
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ns.Dispose();
|
||||
client.Close();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Send_WithSendInFlight_GrowsAndStreamStaysIntact()
|
||||
{
|
||||
|
|
@ -628,58 +596,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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*)#
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,17 +7,6 @@ namespace Server.Tests;
|
|||
[Collection("Sequential Server Tests")]
|
||||
public class TestStringHelpers
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null, "default value", "default value")]
|
||||
[InlineData("", "default value", "default value")]
|
||||
[InlineData("this is a valid string", "default value", "this is a valid string")]
|
||||
public void TestIsNullOrDefault(string value, string defaultValue, string expected)
|
||||
{
|
||||
var actual = value.DefaultIfNullOrEmpty(defaultValue);
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("this is not capitalized", "This Is Not Capitalized")]
|
||||
[InlineData("", "")]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue