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("", "")]
|
||||
|
|
|
|||
|
|
@ -20,15 +20,4 @@ public class AccountHandlerTests
|
|||
{
|
||||
Assert.Equal(expected, AccountHandler.IsValidUsername(username));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", false)] // Empty password
|
||||
[InlineData("ValidPass123!", true)] // Standard Password
|
||||
[InlineData(" ", true)] // Single space
|
||||
[InlineData("ValidPass!@#", true)] // Valid special characters
|
||||
[InlineData("ValidPassWithLength1234567890", true)] // Long valid password
|
||||
public void IsValidPassword_ValidatesCorrectly(string password, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, AccountHandler.IsValidPassword(password));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,18 +152,6 @@ public class PasswordWorkerTests : IDisposable
|
|||
Assert.Null(outcome.Hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppliesAWrite()
|
||||
{
|
||||
var account = CreateAccount("offloop-apply-user");
|
||||
var upgraded = Argon2PasswordProtection.Instance.EncryptPassword(Password);
|
||||
|
||||
account.ApplyPasswordWrite(upgraded, PasswordProtectionAlgorithm.Argon2);
|
||||
|
||||
Assert.Equal(upgraded, account.Password);
|
||||
Assert.True(account.CheckPassword(Password));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes apply in dispatch order, which is what makes a guard unnecessary: dispatch is on the
|
||||
/// loop, one worker drains FIFO, and results return through the loop context in that same order.
|
||||
|
|
@ -217,13 +205,4 @@ public class PasswordWorkerTests : IDisposable
|
|||
AccountSecurity.CurrentAlgorithm = PasswordProtectionAlgorithm.Argon2;
|
||||
Assert.Equal(Password, account.GetRehashPhrase(Password));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsesTheBarePasswordForArgon2Accounts()
|
||||
{
|
||||
var account = CreateAccount("offloop-phrase-argon2-user");
|
||||
|
||||
Assert.Equal(Password, account.GetVerifyPhrase(Password));
|
||||
Assert.Equal(Password, account.GetRehashPhrase(Password));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,13 +150,4 @@ public class PasswordProtectionTest
|
|||
{
|
||||
Assert.True(Argon2PasswordProtection.Instance.NeedsRehash(hash));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonArgon2Protections_NeverNeedRehash()
|
||||
{
|
||||
Assert.False(PBKDF2PasswordProtection.Instance.NeedsRehash("anything"));
|
||||
Assert.False(HashAlgorithmPasswordProtection.SHA2Instance.NeedsRehash("anything"));
|
||||
Assert.False(HashAlgorithmPasswordProtection.SHA1Instance.NeedsRehash("anything"));
|
||||
Assert.False(HashAlgorithmPasswordProtection.MD5Instance.NeedsRehash("anything"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,19 +109,4 @@ public class ChainedBindingSortTests : IDisposable
|
|||
}
|
||||
|
||||
// An unchained binding never had the problem and must keep working untouched.
|
||||
[Fact]
|
||||
public void SortingOnAPlainBindingIsUnchanged()
|
||||
{
|
||||
var low = Teleporter(null);
|
||||
low.Hue = 1;
|
||||
|
||||
var high = Teleporter(null);
|
||||
high.Hue = 2;
|
||||
|
||||
var comparer = Sorter("Hue");
|
||||
|
||||
Assert.True(comparer.Compare(low, high) < 0);
|
||||
Assert.True(comparer.Compare(high, low) > 0);
|
||||
Assert.Equal(0, comparer.Compare(low, low));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,25 +79,4 @@ public class EmitterRobustnessTests
|
|||
Assert.True(Check("Big", ComparisonOperator.Greater, "2147483647"));
|
||||
Assert.True(Check("Huge", ComparisonOperator.Greater, "9223372036854775807"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmittedAssembliesAreCollectible()
|
||||
{
|
||||
var prop = new Property("Tiny");
|
||||
prop.BindTo(typeof(Subject), PropertyAccess.Read);
|
||||
|
||||
var compiled = ConditionalCompiler.Build(
|
||||
typeof(Subject),
|
||||
[TypeCondition.Default, new ComparisonCondition(prop, false, ComparisonOperator.Equal, "5")]
|
||||
).Compile();
|
||||
|
||||
var method = compiled.Method;
|
||||
|
||||
Assert.True(
|
||||
method.IsCollectible,
|
||||
"The conditional compiler compiles one delegate per command invocation; code that is "
|
||||
+ $"not collectible ({method.GetType().Name} in {method.Module}) can never be unloaded, "
|
||||
+ "so every [global where would grow the process for good."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,26 +253,6 @@ public class RecurrencePatternTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonthlyOrdinalRecurrencePattern_DST_FallBack_HandlesAmbiguousTimeCorrectly()
|
||||
{
|
||||
// This tests specifically the DST fall back case that was failing
|
||||
var tz = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
|
||||
var pattern = new MonthlyOrdinalRecurrencePattern(OrdinalDayOccurrence.First, DayOfWeek.Sunday);
|
||||
|
||||
// November 3, 2024 at 1:30 AM - First Sunday, falls on DST transition
|
||||
var beforeDate = new DateTime(2024, 11, 2, 12, 0, 0);
|
||||
var beforeUtc = beforeDate.LocalToUtc(tz);
|
||||
var timeOnly = new TimeOnly(1, 30);
|
||||
|
||||
// Act
|
||||
var result = pattern.GetNextOccurrence(beforeUtc, timeOnly, tz);
|
||||
|
||||
// Assert - should be November 3, 2024 at 1:30 AM
|
||||
var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(result, tz);
|
||||
Assert.Equal(new DateTime(2024, 11, 3, 1, 30, 0), resultLocal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonthlyRecurrence_MonthWithoutDay_SkipsToNextValidMonth()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -67,22 +67,6 @@ public class FactionRankTests
|
|||
Assert.True(top.Rank.Rank > bottom.Rank.Rank);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadingRank_IsStableAndSideEffectFree()
|
||||
{
|
||||
var faction = NewFaction();
|
||||
var owner = new List<PlayerState>();
|
||||
var state = AddMember(faction, owner);
|
||||
|
||||
faction.ZeroRankOffset = 1;
|
||||
state.RankIndex = 0;
|
||||
|
||||
var first = state.Rank;
|
||||
var second = state.Rank;
|
||||
|
||||
Assert.Same(first, second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RankIndexOutOfSyncWithZeroRankOffset_StillResolvesARank()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Server;
|
||||
using Server.Engines.Harvest;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Random;
|
||||
using Server.Targeting;
|
||||
using Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -14,261 +12,9 @@ namespace UOContent.Tests;
|
|||
[Collection("Sequential UOContent Tests")]
|
||||
public class FishingTests
|
||||
{
|
||||
private const int Casts = 100_000;
|
||||
|
||||
// On land in Britain, so a cast there never counts as deep water.
|
||||
private static readonly Point3D _shallow = new(1434, 1699, 0);
|
||||
|
||||
// Open sea with no land within 30 tiles: deep water once the client's map is loaded.
|
||||
private static readonly Point3D _deep = new(1840, 1760, -5);
|
||||
|
||||
private static readonly Type[] _magicFish =
|
||||
[typeof(PrizedFish), typeof(WondrousFish), typeof(TrulyRareFish), typeof(PeculiarFish)];
|
||||
|
||||
private static readonly Type[] _footwear = [typeof(Boots), typeof(Shoes), typeof(Sandals), typeof(ThighBoots)];
|
||||
|
||||
[Theory]
|
||||
[InlineData(Expansion.None, 50.0, 0.0, 0.0)]
|
||||
[InlineData(Expansion.T2A, 0.0, 0.0, 0.75)]
|
||||
[InlineData(Expansion.T2A, 50.0, 0.0, 0.3877)]
|
||||
[InlineData(Expansion.T2A, 100.0, 0.025, 0.0247)]
|
||||
[InlineData(Expansion.UOTD, 80.0, 0.02, 0.1669)]
|
||||
[InlineData(Expansion.LBR, 0.0, 0.0, 0.75)]
|
||||
[InlineData(Expansion.LBR, 50.0, 0.025, 0.4519)]
|
||||
[InlineData(Expansion.AOS, 100.0, 0.05, 0.095)]
|
||||
[InlineData(Expansion.SA, 50.0, 0.025, 0.4519)]
|
||||
[InlineData(Expansion.HS, 50.0, 0.0, 0.4635)]
|
||||
[InlineData(Expansion.HS, 120.0, 0.06, 0.047)]
|
||||
[InlineData(Expansion.EJ, 120.0, 0.06, 0.047)]
|
||||
public void MutateType_MagicFishAndFootwear_FollowTheEra(
|
||||
Expansion expansion, double skill, double magicFishRate, double footwearRate
|
||||
)
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousGenerator = BuiltInRng.Generator;
|
||||
var fisher = new PlayerMobile(World.NewMobile);
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
BuiltInRng.Generator = new System.Random(1);
|
||||
fisher.DefaultMobileInit();
|
||||
fisher.Skills.Fishing.Base = skill;
|
||||
|
||||
Assert.Equal(skill, fisher.Skills.Fishing.Value);
|
||||
Assert.False(SpecialFishingNet.FullValidation(Map.Felucca, _shallow.X, _shallow.Y));
|
||||
|
||||
var definition = Fishing.System.Definitions[0];
|
||||
var magicFish = 0;
|
||||
var footwear = 0;
|
||||
|
||||
for (var i = 0; i < Casts; i++)
|
||||
{
|
||||
var type = Fishing.System.MutateType(typeof(Fish), fisher, null, definition, Map.Felucca, _shallow, null);
|
||||
|
||||
if (Array.IndexOf(_magicFish, type) >= 0)
|
||||
{
|
||||
magicFish++;
|
||||
}
|
||||
else if (Array.IndexOf(_footwear, type) >= 0)
|
||||
{
|
||||
footwear++;
|
||||
}
|
||||
}
|
||||
|
||||
AssertRate(magicFishRate, magicFish);
|
||||
AssertRate(footwearRate, footwear);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fisher.Delete();
|
||||
BuiltInRng.Generator = previousGenerator;
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Leviathan_CarriesItsTreasureMapAQuarterOfTheTime()
|
||||
{
|
||||
var leviathan = new Leviathan();
|
||||
var serpent = new SeaSerpent();
|
||||
|
||||
try
|
||||
{
|
||||
Assert.Equal(0.25, leviathan.TreasureMapChance);
|
||||
Assert.Equal(TreasureMap.LootChance, serpent.TreasureMapChance);
|
||||
}
|
||||
finally
|
||||
{
|
||||
leviathan.Delete();
|
||||
serpent.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Expansion.None, 0.0)]
|
||||
[InlineData(Expansion.T2A, 100.0)]
|
||||
[InlineData(Expansion.UOR, 50.0)]
|
||||
[InlineData(Expansion.LBR, 0.0)]
|
||||
[InlineData(Expansion.EJ, 120.0)]
|
||||
public void MutateType_NeverComesBackEmpty(Expansion expansion, double skill)
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousGenerator = BuiltInRng.Generator;
|
||||
var fisher = new PlayerMobile(World.NewMobile);
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
BuiltInRng.Generator = new System.Random(1);
|
||||
fisher.DefaultMobileInit();
|
||||
fisher.Skills.Fishing.Base = skill;
|
||||
|
||||
var definition = Fishing.System.Definitions[0];
|
||||
|
||||
for (var i = 0; i < Casts; i++)
|
||||
{
|
||||
var type = Fishing.System.MutateType(typeof(Fish), fisher, null, definition, Map.Felucca, _shallow, null);
|
||||
Assert.NotNull(type);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
fisher.Delete();
|
||||
BuiltInRng.Generator = previousGenerator;
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableTheory]
|
||||
[InlineData(Expansion.T2A, 100.0, 0.005, 0.004975)]
|
||||
[InlineData(Expansion.UOTD, 100.0, 0.00495, 0.004925)]
|
||||
[InlineData(Expansion.LBR, 95.0, 0.0062, 0.0)]
|
||||
[InlineData(Expansion.LBR, 100.0, 0.00825, 0.01309)]
|
||||
[InlineData(Expansion.HS, 120.0, 0.016335, 0.0257)]
|
||||
[InlineData(Expansion.EJ, 120.0, 0.016335, 0.0257)]
|
||||
public void MutateType_DeepWaterMapsAndBottles_FollowTheEra(
|
||||
Expansion expansion, double skill, double mapRate, double bottleRate
|
||||
)
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
Assert.True(SpecialFishingNet.FullValidation(Map.Felucca, _deep.X, _deep.Y));
|
||||
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousGenerator = BuiltInRng.Generator;
|
||||
var fisher = new PlayerMobile(World.NewMobile);
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
BuiltInRng.Generator = new System.Random(1);
|
||||
fisher.DefaultMobileInit();
|
||||
fisher.Skills.Fishing.Base = skill;
|
||||
|
||||
var definition = Fishing.System.Definitions[0];
|
||||
var maps = 0;
|
||||
var bottles = 0;
|
||||
|
||||
for (var i = 0; i < Casts; i++)
|
||||
{
|
||||
var type = Fishing.System.MutateType(typeof(Fish), fisher, null, definition, Map.Felucca, _deep, null);
|
||||
|
||||
if (type == typeof(TreasureMap))
|
||||
{
|
||||
maps++;
|
||||
}
|
||||
else if (type == typeof(MessageInABottle))
|
||||
{
|
||||
bottles++;
|
||||
}
|
||||
}
|
||||
|
||||
AssertRate(mapRate, maps);
|
||||
AssertRate(bottleRate, bottles);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fisher.Delete();
|
||||
BuiltInRng.Generator = previousGenerator;
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableTheory]
|
||||
[InlineData(Expansion.T2A, true, 0.0, 100.0)]
|
||||
[InlineData(Expansion.UOR, false, 0.0, 100.0)]
|
||||
[InlineData(Expansion.UOR, true, 65.0, 115.0)]
|
||||
[InlineData(Expansion.EJ, true, 65.0, 115.0)]
|
||||
public void MutateResource_DeepWaterIsHarderFromUOR(Expansion expansion, bool deep, double minSkill, double maxSkill)
|
||||
{
|
||||
if (deep)
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
Assert.True(SpecialFishingNet.FullValidation(Map.Felucca, _deep.X, _deep.Y));
|
||||
}
|
||||
|
||||
var previousExpansion = Core.Expansion;
|
||||
var fisher = new PlayerMobile(World.NewMobile);
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
fisher.DefaultMobileInit();
|
||||
|
||||
var definition = Fishing.System.Definitions[0];
|
||||
var vein = definition.Veins[0];
|
||||
var resource = Fishing.System.MutateResource(
|
||||
fisher, null, definition, Map.Felucca, deep ? _deep : _shallow, vein, vein.PrimaryResource, null
|
||||
);
|
||||
|
||||
Assert.Equal(minSkill, resource.MinSkill);
|
||||
Assert.Equal(maxSkill, resource.MaxSkill);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fisher.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Expansion.T2A, 0.0)]
|
||||
[InlineData(Expansion.UOTD, 0.0)]
|
||||
[InlineData(Expansion.LBR, 0.1)]
|
||||
[InlineData(Expansion.EJ, 0.1)]
|
||||
public void SpecialFishingNet_IsColoredOneTimeInTenFromLBR(Expansion expansion, double coloredRate)
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousGenerator = BuiltInRng.Generator;
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
BuiltInRng.Generator = new System.Random(1);
|
||||
|
||||
var colored = 0;
|
||||
|
||||
for (var i = 0; i < Casts; i++)
|
||||
{
|
||||
var net = new SpecialFishingNet();
|
||||
|
||||
if (net.Hue != 0x8A0)
|
||||
{
|
||||
colored++;
|
||||
}
|
||||
|
||||
net.Delete();
|
||||
}
|
||||
|
||||
AssertRate(coloredRate, colored);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BuiltInRng.Generator = previousGenerator;
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableTheory]
|
||||
[InlineData(1840, 1760, true)] // land 115 tiles away
|
||||
[InlineData(1660, 1760, true)] // land 14 tiles away
|
||||
|
|
@ -284,72 +30,6 @@ public class FishingTests
|
|||
Assert.Equal(deep, SpecialFishingNet.FullValidation(Map.Felucca, x, y));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void SOS_FindLocation_PicksDeepWater()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
|
||||
var previousGenerator = BuiltInRng.Generator;
|
||||
|
||||
try
|
||||
{
|
||||
BuiltInRng.Generator = new System.Random(1);
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
var location = SOS.FindLocation(Map.Felucca);
|
||||
|
||||
Assert.NotEqual(Point3D.Zero, location);
|
||||
Assert.True(SpecialFishingNet.FullValidation(Map.Felucca, location.X, location.Y));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
BuiltInRng.Generator = previousGenerator;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Give_SurfacesASerpentThatAttacksAndIsADeepSeaSerpentOneTimeInTen()
|
||||
{
|
||||
const int serpents = 2_000;
|
||||
|
||||
var previousGenerator = BuiltInRng.Generator;
|
||||
var fisher = new PlayerMobile();
|
||||
|
||||
try
|
||||
{
|
||||
BuiltInRng.Generator = new System.Random(1);
|
||||
fisher.MoveToWorld(_deep, Map.Felucca);
|
||||
|
||||
var deepSeaSerpents = 0;
|
||||
|
||||
for (var i = 0; i < serpents; i++)
|
||||
{
|
||||
var bottle = new MessageInABottle(Map.Felucca);
|
||||
|
||||
Assert.True(Fishing.System.Give(fisher, bottle, false));
|
||||
|
||||
var serpent = Assert.IsAssignableFrom<BaseCreature>(bottle.RootParent);
|
||||
Assert.Same(fisher, serpent.Combatant);
|
||||
|
||||
if (serpent is DeepSeaSerpent)
|
||||
{
|
||||
deepSeaSerpents++;
|
||||
}
|
||||
|
||||
serpent.Delete();
|
||||
}
|
||||
|
||||
AssertRate(0.1, deepSeaSerpents, serpents);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fisher.Delete();
|
||||
BuiltInRng.Generator = previousGenerator;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, false)]
|
||||
[InlineData(2.5, false)]
|
||||
|
|
@ -403,106 +83,39 @@ public class FishingTests
|
|||
}
|
||||
}
|
||||
|
||||
[SkippableTheory]
|
||||
[InlineData(Expansion.None, 100.0, true, 0.0)]
|
||||
[InlineData(Expansion.T2A, 79.9, true, 0.0)]
|
||||
[InlineData(Expansion.T2A, 80.0, true, 0.01)]
|
||||
[InlineData(Expansion.T2A, 100.0, false, 0.0)]
|
||||
[InlineData(Expansion.EJ, 120.0, true, 0.01)]
|
||||
public void OnHarvestFinished_SurfacesAnEmptySerpentOneCastInAHundred(
|
||||
Expansion expansion, double skill, bool deep, double serpentRate
|
||||
)
|
||||
[Fact]
|
||||
public void Construct_SOSChestCatchConsumesTheSOS()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
Assert.True(SpecialFishingNet.FullValidation(Map.Felucca, _deep.X, _deep.Y));
|
||||
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousGenerator = BuiltInRng.Generator;
|
||||
var fisher = new PlayerMobile();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
Core.Expansion = Expansion.UOR;
|
||||
BuiltInRng.Generator = new System.Random(1);
|
||||
fisher.Skills.Fishing.Base = skill;
|
||||
fisher.MoveToWorld(_deep, Map.Felucca);
|
||||
|
||||
var definition = Fishing.System.Definitions[0];
|
||||
var target = new LandTarget(deep ? _deep : _shallow, Map.Felucca);
|
||||
var surfaced = new List<BaseCreature>();
|
||||
var serpents = 0;
|
||||
|
||||
for (var i = 0; i < Casts; i++)
|
||||
{
|
||||
Fishing.System.OnHarvestFinished(fisher, null, definition, null, null, null, target);
|
||||
|
||||
foreach (var serpent in Map.Felucca.GetMobilesInRange<BaseCreature>(fisher.Location, 10))
|
||||
{
|
||||
surfaced.Add(serpent);
|
||||
}
|
||||
|
||||
serpents += surfaced.Count;
|
||||
|
||||
foreach (var serpent in surfaced)
|
||||
{
|
||||
serpent.Delete();
|
||||
}
|
||||
|
||||
surfaced.Clear();
|
||||
}
|
||||
|
||||
AssertRate(serpentRate, serpents);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fisher.Delete();
|
||||
BuiltInRng.Generator = previousGenerator;
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Expansion.T2A, 0.0)]
|
||||
[InlineData(Expansion.UOR, 65.0)]
|
||||
[InlineData(Expansion.EJ, 120.0)]
|
||||
public void Construct_BringsUpTheSOSChestOneCatchInFour(Expansion expansion, double skill)
|
||||
{
|
||||
const int casts = 2_000;
|
||||
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousGenerator = BuiltInRng.Generator;
|
||||
var fisher = new PlayerMobile();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
BuiltInRng.Generator = new System.Random(1);
|
||||
fisher.Skills.Fishing.Base = skill;
|
||||
fisher.Skills.Fishing.Base = 65.0;
|
||||
fisher.AddItem(new Backpack());
|
||||
fisher.MoveToWorld(_deep, Map.Felucca);
|
||||
|
||||
var sos = AddSOS(fisher);
|
||||
var chests = 0;
|
||||
|
||||
for (var i = 0; i < casts; i++)
|
||||
for (var i = 0; i < 200; i++)
|
||||
{
|
||||
var item = Fishing.System.Construct(typeof(Fish), fisher);
|
||||
var isChest = item is WoodenChest or MetalGoldenChest;
|
||||
item.Delete();
|
||||
|
||||
if (item is WoodenChest or MetalGoldenChest)
|
||||
if (isChest)
|
||||
{
|
||||
chests++;
|
||||
Assert.True(sos.Deleted);
|
||||
sos = AddSOS(fisher);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(Assert.IsAssignableFrom<IShipwreckedItem>(item).IsShipwreckedItem);
|
||||
return;
|
||||
}
|
||||
|
||||
item.Delete();
|
||||
Assert.False(sos.Deleted);
|
||||
}
|
||||
|
||||
AssertRate(0.25, chests, casts);
|
||||
Assert.Fail("No SOS chest surfaced in 200 casts");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -512,283 +125,6 @@ public class FishingTests
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Expansion.T2A, 0.0, true)]
|
||||
[InlineData(Expansion.UOR, 64.9, false)]
|
||||
[InlineData(Expansion.UOR, 65.0, true)]
|
||||
[InlineData(Expansion.EJ, 64.9, false)]
|
||||
public void SOS_Needs65SkillFromUOR(Expansion expansion, double skill, bool recovers)
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var definition = Fishing.System.Definitions[0];
|
||||
var fisher = new PlayerMobile();
|
||||
Item item = null;
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
fisher.Skills.Fishing.Base = skill;
|
||||
fisher.AddItem(new Backpack());
|
||||
fisher.MoveToWorld(_deep, Map.Felucca);
|
||||
AddSOS(fisher);
|
||||
|
||||
// With no fish left, only an SOS that works lets the cast go on.
|
||||
var bank = definition.GetBank(Map.Felucca, _deep.X, _deep.Y);
|
||||
bank.Consume(bank.Current, fisher);
|
||||
Assert.Equal(recovers, Fishing.System.CheckResources(fisher, null, definition, Map.Felucca, _deep, false));
|
||||
|
||||
item = Fishing.System.Construct(typeof(Fish), fisher);
|
||||
Assert.Equal(recovers, item is not Fish);
|
||||
}
|
||||
finally
|
||||
{
|
||||
item?.Delete();
|
||||
fisher.Delete();
|
||||
definition.Banks.GetValueOrDefault(Map.Felucca)
|
||||
?.Remove(new Point2D(_deep.X / definition.BankWidth, _deep.Y / definition.BankHeight));
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Construct_KeepsTheWhitePearlAtAnSOSSpot()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var fisher = new PlayerMobile();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.ML;
|
||||
fisher.Skills.Fishing.Base = 100.0;
|
||||
fisher.AddItem(new Backpack());
|
||||
fisher.MoveToWorld(_deep, Map.Felucca);
|
||||
|
||||
var sos = AddSOS(fisher);
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
var item = Fishing.System.Construct(typeof(WhitePearl), fisher);
|
||||
Assert.IsType<WhitePearl>(item);
|
||||
item.Delete();
|
||||
}
|
||||
|
||||
Assert.False(sos.Deleted);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fisher.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Expansion.None, false)]
|
||||
[InlineData(Expansion.T2A, true)]
|
||||
[InlineData(Expansion.AOS, true)]
|
||||
[InlineData(Expansion.ML, true)]
|
||||
public void OnHarvestStartedAndFinished_RevealAHiddenFishermanFromT2A(Expansion expansion, bool reveals)
|
||||
{
|
||||
Core._tickCount = 0;
|
||||
Timer.Init(0);
|
||||
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousNow = Core._now;
|
||||
var fisher = new PlayerMobile();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
fisher.MoveToWorld(_deep, Map.Felucca);
|
||||
|
||||
var definition = Fishing.System.Definitions[0];
|
||||
var target = new LandTarget(_deep, Map.Felucca);
|
||||
|
||||
fisher.Hidden = true;
|
||||
Fishing.System.OnHarvestStarted(fisher, null, definition, target);
|
||||
Advance(TimeSpan.FromSeconds(2.0));
|
||||
Assert.Equal(!reveals, fisher.Hidden);
|
||||
|
||||
fisher.Hidden = true;
|
||||
Fishing.System.OnHarvestFinished(fisher, null, definition, null, null, null, target);
|
||||
Assert.Equal(!reveals, fisher.Hidden);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fisher.Delete();
|
||||
Core._now = previousNow;
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Expansion.UOR, true)]
|
||||
[InlineData(Expansion.AOS, true)]
|
||||
[InlineData(Expansion.SE, false)]
|
||||
public void BigFish_WeighsAHundredStonesBeforeSE(Expansion expansion, bool weighsAHundred)
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousGenerator = BuiltInRng.Generator;
|
||||
var weights = new HashSet<int>();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
BuiltInRng.Generator = new System.Random(1);
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
var fish = new BigFish();
|
||||
weights.Add((int)fish.Weight);
|
||||
fish.Delete();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
BuiltInRng.Generator = previousGenerator;
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
|
||||
Assert.All(weights, weight => Assert.InRange(weight, 3, 200));
|
||||
Assert.Equal(weighsAHundred, weights.SetEquals([100]));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Expansion.AOS, 100, 25)]
|
||||
[InlineData(Expansion.AOS, 12, 4)]
|
||||
[InlineData(Expansion.SE, 100, 20)]
|
||||
[InlineData(Expansion.SE, 200, 40)]
|
||||
[InlineData(Expansion.SE, 12, 4)]
|
||||
public void BigFish_GivesASteakPerFiveStonesFromSE(Expansion expansion, int weight, int steaks)
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var fisher = new PlayerMobile();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
fisher.AddItem(new Backpack());
|
||||
|
||||
var fish = new BigFish { Weight = weight };
|
||||
fisher.Backpack.DropItem(fish);
|
||||
fish.Carve(fisher, null);
|
||||
|
||||
Assert.True(fish.Deleted);
|
||||
Assert.Equal(steaks, fisher.Backpack.FindItemByType<RawFishSteak>()?.Amount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fisher.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Expansion.AOS, 100.0, 0.0)]
|
||||
[InlineData(Expansion.SE, 99.9, 0.0)]
|
||||
[InlineData(Expansion.SE, 100.0, 0.25)]
|
||||
public void Leviathan_GivesAGMNetThrowerAnArtifactOneTimeInFourFromSE(Expansion expansion, double skill, double rate)
|
||||
{
|
||||
const int leviathans = 400;
|
||||
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousGenerator = BuiltInRng.Generator;
|
||||
var fisher = new PlayerMobile();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
BuiltInRng.Generator = new System.Random(1);
|
||||
fisher.Skills.Fishing.Base = skill;
|
||||
fisher.AddItem(new Backpack());
|
||||
fisher.MoveToWorld(_deep, Map.Felucca);
|
||||
|
||||
var pack = fisher.Backpack;
|
||||
var artifacts = 0;
|
||||
|
||||
// Nobody damages it, so only the net thrower's own roll can give an artifact.
|
||||
for (var i = 0; i < leviathans; i++)
|
||||
{
|
||||
var leviathan = new Leviathan(fisher);
|
||||
leviathan.MoveToWorld(_deep, Map.Felucca);
|
||||
leviathan.Kill();
|
||||
|
||||
artifacts += pack.Items.Count;
|
||||
|
||||
for (var j = pack.Items.Count - 1; j >= 0; j--)
|
||||
{
|
||||
pack.Items[j].Delete();
|
||||
}
|
||||
|
||||
leviathan.Corpse?.Delete();
|
||||
leviathan.Delete();
|
||||
}
|
||||
|
||||
AssertRate(rate, artifacts, leviathans);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fisher.Delete();
|
||||
BuiltInRng.Generator = previousGenerator;
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Expansion.AOS, 5, 60)]
|
||||
[InlineData(Expansion.SE, 1, 12)]
|
||||
[InlineData(Expansion.EJ, 1, 12)]
|
||||
public void MagicFish_GiveAPointFor12SecondsFromSE(Expansion expansion, int bonus, int seconds)
|
||||
{
|
||||
Core._tickCount = 0;
|
||||
Timer.Init(0);
|
||||
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousNow = Core._now;
|
||||
var fisher = new PlayerMobile();
|
||||
BaseMagicFish[] fish = [new PrizedFish(), new WondrousFish(), new TrulyRareFish()];
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = expansion;
|
||||
|
||||
foreach (var magicFish in fish)
|
||||
{
|
||||
Assert.True(magicFish.Apply(fisher));
|
||||
Assert.Equal(bonus, fisher.GetStatOffset(magicFish.Type));
|
||||
}
|
||||
|
||||
Advance(TimeSpan.FromSeconds(seconds - 1));
|
||||
Assert.All(fish, magicFish => Assert.Equal(bonus, fisher.GetStatOffset(magicFish.Type)));
|
||||
|
||||
Advance(TimeSpan.FromSeconds(1));
|
||||
Assert.All(fish, magicFish => Assert.Equal(0, fisher.GetStatOffset(magicFish.Type)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var magicFish in fish)
|
||||
{
|
||||
magicFish.Delete();
|
||||
}
|
||||
|
||||
fisher.Delete();
|
||||
Core._now = previousNow;
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
// Within five standard deviations of the expected rate; a rate of zero must never happen.
|
||||
private static void AssertRate(double expected, int count)
|
||||
{
|
||||
var tolerance = 5 * Math.Sqrt(expected * (1 - expected) / Casts);
|
||||
Assert.InRange((double)count / Casts, expected - tolerance, expected + tolerance);
|
||||
}
|
||||
|
||||
private static void AssertRate(double expected, int count, int trials)
|
||||
{
|
||||
var tolerance = 5 * Math.Sqrt(expected * (1 - expected) / trials);
|
||||
Assert.InRange((double)count / trials, expected - tolerance, expected + tolerance);
|
||||
}
|
||||
|
||||
private static void Advance(TimeSpan time)
|
||||
{
|
||||
Core._now += time;
|
||||
|
|
|
|||
|
|
@ -45,31 +45,6 @@ public class BoatPathTests
|
|||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void BoatDeck_FootprintShape_IsPositionInvariant()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
// The property Phase 2's local-frame, movement-invariant boat cache must preserve:
|
||||
// the deck's covered-cell shape (in local coords) is identical at two world positions.
|
||||
var map = Map.Maps[MapId];
|
||||
|
||||
var a = new TestMulti(0x0);
|
||||
map.GetAverageZ(WaterX, WaterY, out _, out var za, out _);
|
||||
a.MoveToWorld(new Point3D(WaterX, WaterY, (sbyte)za), map);
|
||||
var shapeA = LocalShape(a);
|
||||
a.Delete();
|
||||
|
||||
var b = new TestMulti(0x0);
|
||||
map.GetAverageZ(WaterX + 5, WaterY + 3, out _, out var zb, out _);
|
||||
b.MoveToWorld(new Point3D(WaterX + 5, WaterY + 3, (sbyte)zb), map);
|
||||
var shapeB = LocalShape(b);
|
||||
b.Delete();
|
||||
|
||||
Assert.Equal(shapeA, shapeB);
|
||||
Assert.NotEmpty(shapeA);
|
||||
_output.WriteLine($"boat local deck shape stable across positions: {shapeA.Count} cells");
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.HashSet<(int lx, int ly)> LocalShape(BaseMulti multi)
|
||||
{
|
||||
var mcl = multi.Components;
|
||||
|
|
|
|||
|
|
@ -17,28 +17,6 @@ public class FoundationRedesignTests
|
|||
private const int PlaceX = 1500;
|
||||
private const int PlaceY = 1600;
|
||||
|
||||
[SkippableFact]
|
||||
public void SwappingComponents_ChangesFootprint()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var foundation = new SwappableFoundation(0x74); // GuildHouse footprint
|
||||
try
|
||||
{
|
||||
var beforeCount = MultiArt.FootprintCells(foundation).Count;
|
||||
Assert.True(beforeCount > 0, "non-vacuity: initial footprint must cover cells");
|
||||
|
||||
foundation.Redesign(MultiData.GetComponents(0x7A)); // Tower footprint (different shape)
|
||||
var afterCount = MultiArt.FootprintCells(foundation).Count;
|
||||
|
||||
Assert.NotEqual(beforeCount, afterCount);
|
||||
_output.WriteLine($"redesign footprint {beforeCount} -> {afterCount} cells");
|
||||
}
|
||||
finally
|
||||
{
|
||||
foundation.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void RedesignReRegistered_RoutesNewFootprintToLivePath()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -130,33 +130,4 @@ public class MultiEdgeCaseTests
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extensible slot for repo-owner-supplied gnarly placements. The assertion already covers
|
||||
/// any (map,x,y) by construction — only the coordinates need filling in.
|
||||
///
|
||||
/// TODO(coords): repo owner to supply (map,x,y) for a static tree inside a footprint and a
|
||||
/// dungeon cave-wall corner; add InlineData rows here — the assertion already covers them by
|
||||
/// construction. (Left intentionally unhunted: do not invent tree/dungeon coords.)
|
||||
/// </summary>
|
||||
[SkippableTheory]
|
||||
[InlineData(MapId, HouseX, HouseY)] // known-good open Trammel placement (passes today)
|
||||
public void UserSuppliedScenarios_SynthesizerMatchesCheckMovement(int mapId, int x, int y)
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[mapId];
|
||||
|
||||
var multi = new TestMulti(GuildHouseId);
|
||||
map.GetAverageZ(x, y, out _, out var z, out _);
|
||||
multi.MoveToWorld(new Point3D(x, y, (sbyte)z), map);
|
||||
try
|
||||
{
|
||||
MultiTestSupport.AssertSynthesizerMatchesCheckMovement(multi, map);
|
||||
}
|
||||
finally
|
||||
{
|
||||
multi.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,45 +39,6 @@ public class MultiMaskCacheTests
|
|||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void IsInteriorLocalCell_TrueDeepInside_FalseAtEdge()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var multi = new TestMulti(GuildHouseId);
|
||||
try
|
||||
{
|
||||
var mcl = multi.Components;
|
||||
|
||||
var foundInterior = false;
|
||||
var foundEdge = false;
|
||||
for (var ly = 0; ly < mcl.Height && (!foundInterior || !foundEdge); ly++)
|
||||
{
|
||||
for (var lx = 0; lx < mcl.Width; lx++)
|
||||
{
|
||||
if (mcl.Tiles[lx][ly].Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (MultiMaskCache.IsInteriorLocalCell(mcl, lx, ly))
|
||||
{
|
||||
foundInterior = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
foundEdge = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(foundInterior, "a guild house must have at least one interior cell");
|
||||
Assert.True(foundEdge, "a guild house must have at least one edge/perimeter cell");
|
||||
}
|
||||
finally
|
||||
{
|
||||
multi.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LocalWorldZ_RoundTrips_AndPreservesMasks()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -228,17 +228,6 @@ public class StepCacheFileFormatTests
|
|||
Assert.True(fileLength < 1100, $"expected every base Z array to elide; file was {fileLength} bytes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoastlineChunk_ElidesEveryZArray()
|
||||
{
|
||||
// Partial walkability with a non-zero SourceZ: the shape that defeats an unmasked predictor.
|
||||
var src = FlatFullChunk(baseZ: 25);
|
||||
var rt = RoundTrip(src, out var fileLength);
|
||||
|
||||
AssertIdentical(src, rt);
|
||||
Assert.True(fileLength < 1100, $"masked predictor should elide every array; file was {fileLength} bytes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeInOneDirection_StoresOnlyThatZArray()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,16 +42,6 @@ public class StepCacheLifecycleTests : IDisposable
|
|||
return chunk;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_OnEmptyCache_LeavesStatsZero()
|
||||
{
|
||||
var stats = FreshCache(2).GetStats();
|
||||
|
||||
Assert.Equal(0, stats.ResidentChunks);
|
||||
Assert.Equal(0L, stats.Hits);
|
||||
Assert.Equal(0L, stats.BuildsTotal);
|
||||
}
|
||||
|
||||
// ---- promotion gate ----
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -78,174 +78,4 @@ public class StepProbeTests
|
|||
Assert.False(result.IsWalkable(Direction.South), "S should be off-map");
|
||||
Assert.False(result.IsWalkable(Direction.Left), "SW should be off-map");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeMaskAt_TrammelOpenPlain_HasFlatCellWithAllDirectionsWalkable()
|
||||
{
|
||||
// Invariant: somewhere in the open-plain region, there must be a fully-flat cell
|
||||
// whose mask is 0xFF and all 8 destZ values equal sourceZ (no slope).
|
||||
// This protects against a regression where the baker stops detecting flat cells.
|
||||
var map = Map.Maps[1];
|
||||
Assert.NotNull(map);
|
||||
|
||||
var found = false;
|
||||
|
||||
for (var x = 1500; x < 1532 && !found; x++)
|
||||
{
|
||||
for (var y = 1600; y < 1632 && !found; y++)
|
||||
{
|
||||
map.GetAverageZ(x, y, out _, out var avgZ, out _);
|
||||
var sourceZ = (sbyte)avgZ;
|
||||
var result = StepProbe.ComputeMaskAt(map, x, y, sourceZ);
|
||||
|
||||
if (result.WalkMask != 0xFF)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var allSameZ = true;
|
||||
for (var d = 0; d < 8; d++)
|
||||
{
|
||||
if (result.GetWalkZ((Direction)d) != sourceZ)
|
||||
{
|
||||
allSameZ = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allSameZ)
|
||||
{
|
||||
found = true;
|
||||
_output.WriteLine($"Flat-open cell found at ({x}, {y}, {sourceZ})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(found, "Expected at least one fully-flat open cell in (1500..1532, 1600..1632)");
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void ComputeMaskAt_BritainInnDense_HasCellWithBlockedDirections()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
// Invariant: inside the dense Britain inn region, at least one cell must have
|
||||
// at least one direction blocked by a static. Protects against a regression
|
||||
// where the baker reports everything as walkable (the original false-pass bug).
|
||||
var map = Map.Maps[1];
|
||||
Assert.NotNull(map);
|
||||
|
||||
var blockedCellCount = 0;
|
||||
|
||||
for (var x = 1480; x < 1512; x++)
|
||||
{
|
||||
for (var y = 1610; y < 1642; y++)
|
||||
{
|
||||
map.GetAverageZ(x, y, out _, out var avgZ, out _);
|
||||
var sourceZ = (sbyte)avgZ;
|
||||
var result = StepProbe.ComputeMaskAt(map, x, y, sourceZ);
|
||||
|
||||
if (result.WalkMask != 0xFF)
|
||||
{
|
||||
blockedCellCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine($"Cells with at least one blocked direction: {blockedCellCount} / 1024");
|
||||
Assert.True(blockedCellCount > 0, "Expected at least one cell with a blocked direction in the dense region");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1500, 1600)]
|
||||
[InlineData(1480, 1610)]
|
||||
[InlineData(0, 0)]
|
||||
public void ComputeMaskAt_IsDeterministic(int x, int y)
|
||||
{
|
||||
// Same inputs must always produce identical output. Catches accidental
|
||||
// statefulness in the baker (e.g., a static cache that gets corrupted).
|
||||
var map = Map.Maps[1];
|
||||
Assert.NotNull(map);
|
||||
|
||||
map.GetAverageZ(x, y, out _, out var avgZ, out _);
|
||||
var sourceZ = (sbyte)avgZ;
|
||||
|
||||
var first = StepProbe.ComputeMaskAt(map, x, y, sourceZ);
|
||||
var second = StepProbe.ComputeMaskAt(map, x, y, sourceZ);
|
||||
var third = StepProbe.ComputeMaskAt(map, x, y, sourceZ);
|
||||
|
||||
Assert.Equal(first.WalkMask, second.WalkMask);
|
||||
Assert.Equal(first.WalkMask, third.WalkMask);
|
||||
for (var d = 0; d < 8; d++)
|
||||
{
|
||||
Assert.Equal(first.GetWalkZ((Direction)d), second.GetWalkZ((Direction)d));
|
||||
Assert.Equal(first.GetWalkZ((Direction)d), third.GetWalkZ((Direction)d));
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void ComputeMaskAt_PinnedCell_TrammelOpenPlainOrigin()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
// PINNING test: locks specific output for Trammel (1500, 1600).
|
||||
// Cell at z=10 with mask 0xC1 (N + W + NW only walkable). The other five
|
||||
// directions are blocked by water (E/SE/S/SW are wet tiles, NE is shore).
|
||||
// If this changes, either the baker logic changed or the underlying tile
|
||||
// data changed; re-run the parity test to confirm before updating expected values.
|
||||
var map = Map.Maps[1];
|
||||
Assert.NotNull(map);
|
||||
|
||||
map.GetAverageZ(1500, 1600, out _, out var avgZ, out _);
|
||||
var sourceZ = (sbyte)avgZ;
|
||||
|
||||
var result = StepProbe.ComputeMaskAt(map, 1500, 1600, sourceZ);
|
||||
|
||||
Assert.Equal((sbyte)10, sourceZ);
|
||||
Assert.Equal((byte)0xC1, result.WalkMask);
|
||||
|
||||
Assert.True(result.IsWalkable(Direction.North));
|
||||
Assert.False(result.IsWalkable(Direction.Right));
|
||||
Assert.False(result.IsWalkable(Direction.East));
|
||||
Assert.False(result.IsWalkable(Direction.Down));
|
||||
Assert.False(result.IsWalkable(Direction.South));
|
||||
Assert.False(result.IsWalkable(Direction.Left));
|
||||
Assert.True(result.IsWalkable(Direction.West));
|
||||
Assert.True(result.IsWalkable(Direction.Up));
|
||||
|
||||
Assert.Equal((sbyte)10, result.GetWalkZ(Direction.North));
|
||||
Assert.Equal((sbyte)10, result.GetWalkZ(Direction.West));
|
||||
Assert.Equal((sbyte)10, result.GetWalkZ(Direction.Up));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void ComputeMaskAt_PinnedCell_BritainInnDenseOrigin()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
// PINNING test: locks specific output for Trammel (1480, 1610).
|
||||
// Cell at z=20 with mask 0x3F (N/NE/E/SE/S/SW walkable, W/NW blocked by
|
||||
// a wall to the west). All walkable directions stay flat at z=20.
|
||||
var map = Map.Maps[1];
|
||||
Assert.NotNull(map);
|
||||
|
||||
map.GetAverageZ(1480, 1610, out _, out var avgZ, out _);
|
||||
var sourceZ = (sbyte)avgZ;
|
||||
|
||||
var result = StepProbe.ComputeMaskAt(map, 1480, 1610, sourceZ);
|
||||
|
||||
Assert.Equal((sbyte)20, sourceZ);
|
||||
Assert.Equal((byte)0x3F, result.WalkMask);
|
||||
|
||||
Assert.True(result.IsWalkable(Direction.North));
|
||||
Assert.True(result.IsWalkable(Direction.Right));
|
||||
Assert.True(result.IsWalkable(Direction.East));
|
||||
Assert.True(result.IsWalkable(Direction.Down));
|
||||
Assert.True(result.IsWalkable(Direction.South));
|
||||
Assert.True(result.IsWalkable(Direction.Left));
|
||||
Assert.False(result.IsWalkable(Direction.West));
|
||||
Assert.False(result.IsWalkable(Direction.Up));
|
||||
|
||||
for (var d = 0; d < 6; d++)
|
||||
{
|
||||
Assert.Equal((sbyte)20, result.GetWalkZ((Direction)d));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,32 +98,6 @@ public class ImportCleanupTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_MalformedFile_LeaksNoWorldItems()
|
||||
{
|
||||
// First entry is syntactically valid JSON for a Spawner, second entry is truncated/garbage.
|
||||
// STJ throws a JsonException during array deserialization before any Items are created
|
||||
// (the DTO path is GC-only), so zero new spawners should appear at the valid entry's location.
|
||||
var dir = Path.Combine(Path.GetTempPath(), "muo-import-malformed-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(Path.Combine(dir, "bad.json"), """
|
||||
[ { "$type": "Spawner", "location": [320, 320, 0], "map": "Felucca", "count": 1,
|
||||
"entries": [ { "name": "Fisherman", "maxCount": 1, "probability": 100 } ] },
|
||||
{ "$type": "Spawner", "location": [ THIS IS NOT JSON
|
||||
""");
|
||||
try
|
||||
{
|
||||
var before = CountSpawnersNear(new Point3D(320, 320, 0));
|
||||
ImportSpawnersCommand.ImportFile(new FileInfo(Path.Combine(dir, "bad.json")), new Dictionary<Guid, ISpawner>());
|
||||
// Malformed parse must construct zero Items (DTO is GC-only); nothing placed or orphaned.
|
||||
Assert.Equal(before, CountSpawnersNear(new Point3D(320, 320, 0)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(dir, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static int CountSpawnersNear(Point3D p)
|
||||
{
|
||||
var n = 0;
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
// Regression guard: verifies that a real migrated spawn file (post-uoml/felucca/Vendors.json)
|
||||
// can be deserialized as List<SpawnerDto> and each DTO produces a valid spawner via ToSpawner().
|
||||
// Exercises the $type discriminator path introduced by the data migration.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Server;
|
||||
using Server.Engines.Spawners;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests.Engines.Spawners.Json;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class MigratedDataLoadTests
|
||||
{
|
||||
[Fact]
|
||||
public void MigratedFile_LoadsWithDollarType()
|
||||
{
|
||||
var path = Path.Combine(Core.BaseDirectory, "Data", "Spawns", "post-uoml", "felucca", "Vendors.json");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return; // distribution data not present in this checkout
|
||||
}
|
||||
|
||||
var dtos = JsonSerializer.Deserialize<List<SpawnerDto>>(File.ReadAllText(path), SpawnerJsonSerializer.Options);
|
||||
Assert.NotEmpty(dtos);
|
||||
|
||||
var spawners = new List<BaseSpawner>(dtos.Count);
|
||||
try
|
||||
{
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
spawners.Add(dto.ToSpawner());
|
||||
}
|
||||
|
||||
Assert.Equal(dtos.Count, spawners.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in spawners)
|
||||
{
|
||||
s?.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,33 +44,6 @@ public class SpawnerDtoRoundTripTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spawner_HomeRangeSquare_WritesHomeRange_RoundTrips()
|
||||
{
|
||||
Spawner original = null;
|
||||
BaseSpawner rebuilt = null;
|
||||
try
|
||||
{
|
||||
original = new Spawner("Fisherman");
|
||||
original.MoveToWorld(new Point3D(200, 200, 0), Map.Felucca);
|
||||
// Exactly what homeRange 5 reconstructs (centered square, standard z/depth).
|
||||
original.SpawnBounds = new Rectangle3D(195, 195, -128, 11, 11, 256);
|
||||
|
||||
var json = JsonSerializer.Serialize(new List<SpawnerDto> { original.ToDto() }, SpawnerJsonSerializer.Options);
|
||||
Assert.Contains("\"homeRange\": 5", json);
|
||||
Assert.DoesNotContain("spawnBounds", json);
|
||||
|
||||
var dtos = JsonSerializer.Deserialize<List<SpawnerDto>>(json, SpawnerJsonSerializer.Options);
|
||||
rebuilt = Assert.Single(dtos).ToSpawner();
|
||||
Assert.Equal(new Rectangle3D(195, 195, -128, 11, 11, 256), rebuilt.SpawnBounds);
|
||||
}
|
||||
finally
|
||||
{
|
||||
rebuilt?.Delete();
|
||||
original?.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spawner_WritesMandatory_OmitsOptionalDefaults()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,14 +7,6 @@ namespace UOContent.Tests;
|
|||
|
||||
public class BitMask256Tests
|
||||
{
|
||||
[Fact]
|
||||
public void BitMask256_InitialState_AllBitsZero()
|
||||
{
|
||||
var mask = BitMask256.AllClear();
|
||||
|
||||
Assert.Equal(0, mask.PopCount());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(63)]
|
||||
|
|
@ -110,41 +102,6 @@ public class SectorSpawnCacheManagerTests
|
|||
SectorSpawnCacheManager.ClearAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearAll_ResetsCache()
|
||||
{
|
||||
SectorSpawnCacheManager.ClearAll();
|
||||
Assert.Equal(0, SectorSpawnCacheManager.CachedSectorCount);
|
||||
Assert.Equal(0, SectorSpawnCacheManager.LandCacheCount);
|
||||
Assert.Equal(0, SectorSpawnCacheManager.WaterCacheCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetValid_CreatesSectorCache()
|
||||
{
|
||||
var map = Map.Felucca;
|
||||
var pos = new Point3D(100, 100, 0);
|
||||
|
||||
SectorSpawnCacheManager.SetValid(map, pos, isWater: false);
|
||||
|
||||
Assert.Equal(1, SectorSpawnCacheManager.CachedSectorCount);
|
||||
Assert.Equal(1, SectorSpawnCacheManager.LandCacheCount);
|
||||
Assert.Equal(0, SectorSpawnCacheManager.WaterCacheCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetValid_Water_CreatesWaterCache()
|
||||
{
|
||||
var map = Map.Felucca;
|
||||
var pos = new Point3D(100, 100, 0);
|
||||
|
||||
SectorSpawnCacheManager.SetValid(map, pos, isWater: true);
|
||||
|
||||
Assert.Equal(1, SectorSpawnCacheManager.CachedSectorCount);
|
||||
Assert.Equal(0, SectorSpawnCacheManager.LandCacheCount);
|
||||
Assert.Equal(1, SectorSpawnCacheManager.WaterCacheCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetValid_LandAndWater_SeparateCaches()
|
||||
{
|
||||
|
|
@ -251,19 +208,6 @@ public class SectorSpawnCacheManagerTests
|
|||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidateSectors_RemovesCachedData()
|
||||
{
|
||||
var map = Map.Felucca;
|
||||
|
||||
SectorSpawnCacheManager.SetValid(map, new Point3D(50, 50, 0), isWater: false);
|
||||
Assert.Equal(1, SectorSpawnCacheManager.CachedSectorCount);
|
||||
|
||||
SectorSpawnCacheManager.InvalidateSectors(map, new Rectangle2D(0, 0, 100, 100));
|
||||
|
||||
Assert.Equal(0, SectorSpawnCacheManager.CachedSectorCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidateSectors_RemovesBothLandAndWater()
|
||||
{
|
||||
|
|
@ -341,54 +285,6 @@ public class SpiralScanTests
|
|||
Assert.Equal(expectedDy, dy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpiralPattern_Ring1Has8Positions()
|
||||
{
|
||||
// Ring N has 8*N positions
|
||||
const int ring1Positions = 1 * 8;
|
||||
Assert.Equal(8, ring1Positions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpiralPattern_Ring2Has16Positions()
|
||||
{
|
||||
const int ring2Positions = 2 * 8;
|
||||
Assert.Equal(16, ring2Positions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpiralPattern_Ring10Has80Positions()
|
||||
{
|
||||
const int ring10Positions = 10 * 8;
|
||||
Assert.Equal(80, ring10Positions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpiralPattern_Ring1_CoversAllAdjacentTiles()
|
||||
{
|
||||
// Ring 1 should cover all 8 adjacent tiles
|
||||
var expectedOffsets = new HashSet<(int dx, int dy)>
|
||||
{
|
||||
(-1, -1), (0, -1), (1, -1), // Top row
|
||||
(1, 0), // Right
|
||||
(1, 1), (0, 1), (-1, 1), // Bottom row
|
||||
(-1, 0) // Left
|
||||
};
|
||||
|
||||
var actualOffsets = new HashSet<(int dx, int dy)>();
|
||||
|
||||
for (var position = 0; position < 8; position++)
|
||||
{
|
||||
actualOffsets.Add(SectorSpawnCacheManager.GetSpiralOffset(1, position));
|
||||
}
|
||||
|
||||
Assert.Equal(expectedOffsets.Count, actualOffsets.Count);
|
||||
foreach (var expected in expectedOffsets)
|
||||
{
|
||||
Assert.Contains(expected, actualOffsets);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpiralPattern_Ring2_CoversAllExpectedTiles()
|
||||
{
|
||||
|
|
@ -414,16 +310,6 @@ public class SpiralScanTests
|
|||
|
||||
public class SpawnPositionStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void SpawnPositionState_InitialState_ZeroCounts()
|
||||
{
|
||||
var state = new SpawnPositionState();
|
||||
|
||||
Assert.False(state.SpiralComplete);
|
||||
Assert.Equal(0, state.SpiralRing);
|
||||
Assert.Equal(0, state.SpiralRingPosition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsAllState()
|
||||
{
|
||||
|
|
@ -457,14 +343,6 @@ public class SpawnPositionStateTests
|
|||
Assert.False(state.ShouldCachePositions(SpawnPositionMode.Disabled));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCachePositions_Automatic_FalseWithNoFailures()
|
||||
{
|
||||
var state = new SpawnPositionState();
|
||||
|
||||
Assert.False(state.ShouldCachePositions(SpawnPositionMode.Automatic));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCachePositions_Automatic_TrueAfterThresholdFailures()
|
||||
{
|
||||
|
|
@ -522,30 +400,6 @@ public class SpawnPositionStateTests
|
|||
Assert.True(state.ShouldAbandon());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldAbandon_FalseWithUsefulCacheHit()
|
||||
{
|
||||
var state = new SpawnPositionState();
|
||||
state.SpiralComplete = true;
|
||||
|
||||
// Record 24 useless results
|
||||
for (var i = 0; i < 24; i++)
|
||||
{
|
||||
state.RecordUselessResult();
|
||||
}
|
||||
|
||||
// Record a useful cache hit - resets counter
|
||||
state.RecordUsefulCacheHit();
|
||||
|
||||
// Record a few more useless results
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
state.RecordUselessResult();
|
||||
}
|
||||
|
||||
Assert.False(state.ShouldAbandon()); // Counter was reset, only 5 now
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordUsefulCacheHit_ResetsUselessCounter()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -67,18 +67,6 @@ public class SpawnerEntryOwnershipTests
|
|||
spawner.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Start_WorksAfterStop_WhenOwnerHasEntries()
|
||||
{
|
||||
var spawner = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit");
|
||||
Assert.True(spawner.Running);
|
||||
spawner.Stop();
|
||||
Assert.False(spawner.Running);
|
||||
spawner.Start();
|
||||
Assert.True(spawner.Running);
|
||||
spawner.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dupe_ClonesEntriesIntoIndependentList()
|
||||
{
|
||||
|
|
@ -159,85 +147,4 @@ public class SpawnerEntryOwnershipTests
|
|||
}
|
||||
|
||||
// Manual: remove the Skip, run, and read the numbers from the thrown assertion.
|
||||
[Fact(Skip = "manual benchmark")]
|
||||
public void Benchmark_SpawnPath_Manual()
|
||||
{
|
||||
const int iterations = 100_000;
|
||||
const int warmup = 1_000;
|
||||
|
||||
var report = new ValueStringBuilder(stackalloc char[512]);
|
||||
report.Append('\n', 1);
|
||||
|
||||
foreach (var entryCount in new[] { 1, 10, 50 })
|
||||
{
|
||||
var spawner = new Spawner(1000, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10));
|
||||
spawner.MoveToWorld(new Point3D(1500, 1500, 0), Map.Felucca);
|
||||
|
||||
// SpawnedMaxCount = 0 keeps every entry full, so Spawn() does selection only.
|
||||
for (var i = 0; i < entryCount; i++)
|
||||
{
|
||||
spawner.AddEntry("Rabbit", 100, 0, false);
|
||||
}
|
||||
|
||||
for (var i = 0; i < warmup; i++)
|
||||
{
|
||||
spawner.Spawn();
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
spawner.Spawn();
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
var nsPerCall = sw.Elapsed.TotalMilliseconds * 1_000_000.0 / iterations;
|
||||
report.Append(
|
||||
$"Spawn() entries={entryCount,2}: {nsPerCall,8:F1} ns/call ({iterations} iterations, {sw.ElapsedMilliseconds} ms total)\n"
|
||||
);
|
||||
|
||||
spawner.Delete();
|
||||
}
|
||||
|
||||
{
|
||||
var spawner = new Spawner(1000, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10));
|
||||
spawner.MoveToWorld(new Point3D(1500, 1500, 0), Map.Felucca);
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
spawner.AddEntry("Rabbit", 100, 1, false);
|
||||
}
|
||||
|
||||
spawner.Spawn();
|
||||
Assert.Single(spawner.Spawned);
|
||||
var rabbit = spawner.Spawned.Keys.First();
|
||||
|
||||
for (var i = 0; i < warmup; i++)
|
||||
{
|
||||
spawner.Remove(rabbit);
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
spawner.Remove(rabbit);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
var nsPerCall = sw.Elapsed.TotalMilliseconds * 1_000_000.0 / iterations;
|
||||
report.Append(
|
||||
$"Remove(ISpawnable) entries=10: {nsPerCall,8:F1} ns/call ({iterations} iterations, {sw.ElapsedMilliseconds} ms total)\n"
|
||||
);
|
||||
|
||||
(rabbit as Mobile)?.Delete();
|
||||
spawner.Delete();
|
||||
}
|
||||
|
||||
var summary = report.ToString();
|
||||
report.Dispose();
|
||||
|
||||
throw new Xunit.Sdk.XunitException(summary);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Server.Engines.Spawners;
|
||||
using Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests.Engines.Spawners;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class SpawnerFixtureCapture
|
||||
{
|
||||
// Freezes the BaseSpawner v12 save layout as test input; refuses to run against newer code.
|
||||
[SkippableFact]
|
||||
public void CaptureLegacyBlobs()
|
||||
{
|
||||
Skip.If(Environment.GetEnvironmentVariable("MODERNUO_CAPTURE_SPAWNER_FIXTURES") != "1");
|
||||
|
||||
var version = (int)typeof(BaseSpawner)
|
||||
.GetField("SerializationVersion", BindingFlags.NonPublic | BindingFlags.Static)!
|
||||
.GetRawConstantValue()!;
|
||||
Assert.True(version == 12, $"Fixtures must be captured with BaseSpawner v12; current version is {version}.");
|
||||
|
||||
var dir = Path.Combine(AppContext.BaseDirectory, "Tests", "Engines", "Spawners", "Fixtures");
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
var spawner = new Spawner(2, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit", "Bird")
|
||||
{
|
||||
Name = "FixtureSpawner"
|
||||
};
|
||||
spawner.Entries[1].Properties = "Hue 33";
|
||||
File.WriteAllBytes(Path.Combine(dir, "spawner.v12-v1.bin"), SpawnerBlob.Write(spawner));
|
||||
|
||||
var proximity = new ProximitySpawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, 5, "boo", true, "Rat", "Bird")
|
||||
{
|
||||
Name = "FixtureProximity"
|
||||
};
|
||||
File.WriteAllBytes(Path.Combine(dir, "proximity.v12-v1-v0.bin"), SpawnerBlob.Write(proximity));
|
||||
|
||||
var region = new RegionSpawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, "Orc", "Troll")
|
||||
{
|
||||
Name = "FixtureRegion"
|
||||
};
|
||||
File.WriteAllBytes(Path.Combine(dir, "region.v12-v1-v0.bin"), SpawnerBlob.Write(region));
|
||||
|
||||
spawner.Delete();
|
||||
proximity.Delete();
|
||||
region.Delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -103,45 +103,6 @@ public class GridLayoutTests
|
|||
Assert.Equal(800, sizes[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestGridCell_Properties()
|
||||
{
|
||||
var cell = new GridCell(10, 20, 100, 50);
|
||||
|
||||
Assert.Equal(10, cell.X);
|
||||
Assert.Equal(20, cell.Y);
|
||||
Assert.Equal(100, cell.Width);
|
||||
Assert.Equal(50, cell.Height);
|
||||
Assert.Equal(110, cell.Right);
|
||||
Assert.Equal(70, cell.Bottom);
|
||||
Assert.Equal(60, cell.CenterX);
|
||||
Assert.Equal(45, cell.CenterY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestGridCell_Inset()
|
||||
{
|
||||
var cell = new GridCell(10, 20, 100, 50);
|
||||
var inset = cell.Inset(5);
|
||||
|
||||
Assert.Equal(15, inset.X);
|
||||
Assert.Equal(25, inset.Y);
|
||||
Assert.Equal(90, inset.Width);
|
||||
Assert.Equal(40, inset.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestGridCell_Offset()
|
||||
{
|
||||
var cell = new GridCell(10, 20, 100, 50);
|
||||
var offset = cell.Offset(5, 10);
|
||||
|
||||
Assert.Equal(15, offset.X);
|
||||
Assert.Equal(30, offset.Y);
|
||||
Assert.Equal(100, offset.Width);
|
||||
Assert.Equal(50, offset.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestListViewLayout_Pagination()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -73,34 +73,6 @@ public class T2AJewelGemCraftTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnCraft_WithUnsetGemContext_LeavesPlainPiece()
|
||||
{
|
||||
var map = Map.Felucca;
|
||||
var player = CreatePlayerMobile(map, new Point3D(4120, 500, 0));
|
||||
var ring = new GoldRing();
|
||||
|
||||
try
|
||||
{
|
||||
player.Backpack.AddItem(new IronIngot(10));
|
||||
|
||||
var system = GetOrInitTinkeringSystem();
|
||||
var context = system.GetContext(player);
|
||||
context.PendingGemType = GemType.None; // no gem targeted
|
||||
context.PendingGemCount = 0;
|
||||
|
||||
ring.OnCraft(1, false, player, system, typeof(IronIngot), null, MakeRingRecipe(), 0);
|
||||
|
||||
Assert.Equal(GemType.None, ring.GemType);
|
||||
Assert.Equal(0, ring.GemCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ring.Delete();
|
||||
player.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnCraft_WhenGemsUnavailableAtCraftTime_CraftsPlainPiece()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -45,15 +45,4 @@ public class CorpseHairMigrationTests
|
|||
// The entire block must be consumed - the loader validates exact byte length.
|
||||
Assert.Equal(buffer.Length, reader.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyHairTail_Absent_ConsumesOnlyPresenceBool()
|
||||
{
|
||||
var buffer = WriteLegacyHairTail(false, 0, 0);
|
||||
var reader = new BufferReader(buffer);
|
||||
|
||||
Assert.False(reader.ReadBool());
|
||||
// Absent case: nothing else to read, and the presence bool was the only byte.
|
||||
Assert.Equal(buffer.Length, reader.Position);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,27 +19,6 @@ public class TreasureChestDecayTests
|
|||
|
||||
// DecayScheduler reads ScheduledDecayTime repeatedly and needs the reads to agree, so the
|
||||
// random interval must be rolled once per chest rather than per read.
|
||||
[Theory]
|
||||
[MemberData(nameof(ChestTypes))]
|
||||
public void DecayTimeIsStableAcrossReads(Type chestType)
|
||||
{
|
||||
var chest = chestType.CreateInstance<LockableContainer>();
|
||||
|
||||
try
|
||||
{
|
||||
var expected = chest.DecayTime;
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(expected, chest.DecayTime);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
chest.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ChestTypes))]
|
||||
public void ScheduledDecayTimeIsStableAcrossReads(Type chestType)
|
||||
|
|
|
|||
|
|
@ -16,15 +16,6 @@ public class NameVerificationTests
|
|||
Assert.True(NameVerification.ValidatePlayerName(name));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Rex")]
|
||||
[InlineData("MrWhiskers")]
|
||||
[InlineData("Fido")]
|
||||
public void ValidatePetName_ValidNames_ReturnsTrue(string name)
|
||||
{
|
||||
Assert.True(NameVerification.ValidatePetName(name));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Mrs-Smith")]
|
||||
[InlineData("Mr. Whiskers")]
|
||||
|
|
@ -128,10 +119,4 @@ public class NameVerificationTests
|
|||
var exceptions = SearchValues.Create(' ', '-', '.');
|
||||
Assert.False(NameVerification.Validate("-John", 2, 16, true, false, true, 1, exceptions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DisallowedCharacters_ReturnsFalse()
|
||||
{
|
||||
Assert.False(NameVerification.Validate("John123", 2, 16, true, false));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,18 +139,6 @@ public class FamiliarAITests : IDisposable
|
|||
return condition();
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void UsesFamiliarAI_AndStaysOnComeOrder()
|
||||
{
|
||||
var p = Master(1500, 1600);
|
||||
var f = Familiar(0, p, 1500, 1600);
|
||||
|
||||
Assert.IsType<FamiliarAI>(f.AIObject);
|
||||
Assert.Equal(OrderType.Come, f.ControlOrder);
|
||||
Assert.Equal(0.1, f.ActiveSpeed);
|
||||
Assert.Equal(0.1, f.PassiveSpeed);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void Follows_WithoutBacktracking()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,48 +50,4 @@ public class GuardFollowTests
|
|||
Assert.Equal(0.2, currentSpeed);
|
||||
Assert.Equal(0.1, currentMoveSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GuardReturn_PreAOS_RunsActive()
|
||||
{
|
||||
var previous = Core.Expansion;
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.UOR;
|
||||
|
||||
var map = Map.Maps[1];
|
||||
Assert.NotNull(map);
|
||||
map.GetAverageZ(1500, 1600, out _, out var z, out _);
|
||||
|
||||
var master = new PlayerMobile(World.NewMobile);
|
||||
master.DefaultMobileInit();
|
||||
master.MoveToWorld(new Point3D(1494, 1600, (sbyte)z), map);
|
||||
|
||||
var pet = new PetTestStub();
|
||||
pet.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map);
|
||||
pet.SetControlMaster(master);
|
||||
|
||||
var ai = pet.AIObject;
|
||||
ai.AITimer?.Stop();
|
||||
pet.ControlOrder = OrderType.Guard;
|
||||
ai.AITimer?.Stop();
|
||||
pet.SetCurrentSpeedToPassive(); // a stale passive state must not persist
|
||||
|
||||
ai.NextMove = 0;
|
||||
ai.Obey();
|
||||
|
||||
var currentSpeed = pet.CurrentSpeed;
|
||||
|
||||
pet.Delete();
|
||||
master.Delete();
|
||||
|
||||
// No sprint pre-AOS: the return runs active.
|
||||
Assert.Equal(0.2, currentSpeed);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Server;
|
||||
using Server.Mobiles;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests.Mobiles.AI;
|
||||
|
||||
// A meer mage fights FightMode.Evil and its enraged creatures carry -1000 karma, so the karma rules alone
|
||||
// make each an enemy of the other.
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class MeerEnragedTests : IDisposable
|
||||
{
|
||||
private readonly List<Mobile> _created = [];
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
for (var i = 0; i < _created.Count; i++)
|
||||
{
|
||||
_created[i].Delete();
|
||||
}
|
||||
}
|
||||
|
||||
private T Track<T>(T m) where T : Mobile
|
||||
{
|
||||
_created.Add(m);
|
||||
return m;
|
||||
}
|
||||
|
||||
private static void Place(BaseCreature bc, int x)
|
||||
{
|
||||
var map = Map.Felucca;
|
||||
bc.MoveToWorld(new Point3D(x, 1600, map.GetAverageZ(x, 1600)), map); // flat, clear line of sight both ways
|
||||
bc.AIObject.AITimer.Stop();
|
||||
}
|
||||
|
||||
private static bool Acquire(BaseCreature creature)
|
||||
{
|
||||
creature.NextReacquireTime = Core.TickCount - 1;
|
||||
return creature.AIObject.AcquireFocusMob(creature.RangePerception, creature.FightMode, false, false, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeerAndItsEnragedCreature_AreNotEnemies()
|
||||
{
|
||||
var meer = Track(new MeerMage());
|
||||
var enraged = Track(new EnragedRabbit(meer));
|
||||
|
||||
Assert.False(enraged.IsEnemy(meer));
|
||||
Assert.False(meer.IsEnemy(enraged));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnotherMeersEnragedCreature_StaysAnEnemy()
|
||||
{
|
||||
var meer = Track(new MeerMage());
|
||||
var other = Track(new MeerMage());
|
||||
var enraged = Track(new EnragedRabbit(other));
|
||||
|
||||
Assert.True(enraged.IsEnemy(meer));
|
||||
Assert.True(meer.IsEnemy(enraged));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void MeerAndItsEnragedCreature_NeverAcquireEachOther()
|
||||
{
|
||||
Skip.If(!Server.Tests.TestServerInitializer.TileDataLoaded, "Requires UO client map data.");
|
||||
|
||||
var meer = Track(new MeerMage());
|
||||
var enraged = Track(new EnragedBlackBear(meer));
|
||||
Place(meer, 1600);
|
||||
Place(enraged, 1601);
|
||||
|
||||
Assert.False(Acquire(enraged));
|
||||
Assert.False(Acquire(meer));
|
||||
}
|
||||
}
|
||||
|
|
@ -265,21 +265,6 @@ public class PetOrderTests : IDisposable
|
|||
Assert.Equal(pet.Location, pet.Home);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoyaltyRelease_ClearsTheMaster_AndStartsTheDeleteCountdown()
|
||||
{
|
||||
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||
var followers = master.Followers;
|
||||
|
||||
// What the loyalty drain assigns when loyalty reaches zero.
|
||||
pet.ControlOrder = OrderType.Release;
|
||||
|
||||
Assert.False(pet.Controlled);
|
||||
Assert.Null(pet.ControlMaster);
|
||||
Assert.Equal(followers - pet.ControlSlots, master.Followers);
|
||||
Assert.True(pet.PendingDeleteTimer?.Running);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Release_WithoutSpawner_AnchorsHomeToCurrentLocation()
|
||||
{
|
||||
|
|
@ -375,29 +360,6 @@ public class PetOrderTests : IDisposable
|
|||
Assert.Same(master, pet.ControlTarget);
|
||||
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stop_WhileFollowing_CancelsToIdle_NonML()
|
||||
{
|
||||
var previous = Core.Expansion;
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SE; // pre-ML: Core.ML is false
|
||||
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1002, 1000, 0));
|
||||
pet.ControlOrder = OrderType.Follow;
|
||||
|
||||
pet.ControlOrder = OrderType.Stop;
|
||||
|
||||
// Stop/idle resolution is era-independent.
|
||||
Assert.Equal(OrderType.None, pet.ControlOrder);
|
||||
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
||||
Assert.Equal(pet.Location, pet.Home);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previous;
|
||||
}
|
||||
}
|
||||
// The parameterless ctor fully initializes a player; the Serial ctor leaves that to Deserialize.
|
||||
private PlayerMobile SpawnPlayer(Point3D loc)
|
||||
{
|
||||
|
|
@ -991,19 +953,6 @@ public class PetOrderTests : IDisposable
|
|||
Assert.Same(friend, pet.ControlTarget);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GMObey_TakesControlOfACommandablePet()
|
||||
{
|
||||
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||
pet.Name = "Rex";
|
||||
var gm = SpawnPlayer(new Point3D(1002, 1000, 0));
|
||||
gm.AccessLevel = AccessLevel.GameMaster;
|
||||
|
||||
pet.AIObject.OnSpeech(new SpeechEventArgs(gm, "Rex obey", MessageType.Regular, 0x3B2, []));
|
||||
|
||||
Assert.Same(gm, pet.ControlMaster);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GMAllObey_DoesNotTakeControlledPets()
|
||||
{
|
||||
|
|
@ -1021,42 +970,5 @@ public class PetOrderTests : IDisposable
|
|||
}
|
||||
|
||||
// Release is relinquishing control, not exerting it: no roll, so no loyalty either way.
|
||||
[Fact]
|
||||
public void MenuRelease_TouchesNoLoyalty()
|
||||
{
|
||||
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||
pet.Loyalty = 50;
|
||||
|
||||
new InternalEntry(3006118, 14, OrderType.Release, true).OnClick(master, pet); // Release
|
||||
|
||||
Assert.Equal(50, pet.Loyalty); // no roll: neither the +1 for passing nor the -3 for failing
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpeechRelease_TouchesNoLoyalty()
|
||||
{
|
||||
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||
pet.Name = "Rex";
|
||||
pet.Loyalty = 50;
|
||||
|
||||
pet.AIObject.OnSpeech(new SpeechEventArgs(master, "Rex release", MessageType.Regular, 0x3B2, [0x16D]));
|
||||
|
||||
Assert.Equal(50, pet.Loyalty);
|
||||
}
|
||||
|
||||
// A creature nobody can command is not released either.
|
||||
[Fact]
|
||||
public void MenuRelease_OnAnUncontrollablePet_IsRefusedWithoutCost()
|
||||
{
|
||||
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||
pet.MinTameSkill = 120.0; // control chance at or below zero
|
||||
master.Skills.AnimalTaming.Base = 0;
|
||||
master.Skills.AnimalLore.Base = 0;
|
||||
pet.Loyalty = 50;
|
||||
|
||||
new InternalEntry(3006118, 14, OrderType.Release, true).OnClick(master, pet);
|
||||
|
||||
Assert.Equal(50, pet.Loyalty); // refused, but never punished
|
||||
Assert.True(pet.Controlled);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,29 +99,6 @@ public class PetPacingTests : IDisposable
|
|||
}
|
||||
|
||||
// Pre-AOS pets follow at their own pace; nothing caps them.
|
||||
[Fact]
|
||||
public void FollowMaster_PreAOS_KeepsItsOwnPace()
|
||||
{
|
||||
var previous = Core.Expansion;
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.UOR;
|
||||
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||
pet.SetMoveSpeed(0.3, 0.9);
|
||||
|
||||
pet.ControlTarget = master;
|
||||
pet.ControlOrder = OrderType.Follow;
|
||||
|
||||
Assert.Equal(0.2, pet.CurrentSpeed);
|
||||
Assert.Equal(0.3, pet.CurrentMoveSpeed);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previous;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SprintingPet : PetTestStub
|
||||
{
|
||||
public override double FollowMoveSpeed => 0.125;
|
||||
|
|
@ -204,35 +181,7 @@ public class PetPacingTests : IDisposable
|
|||
}
|
||||
|
||||
// A pet chasing a combatant keeps the move table.
|
||||
[Fact]
|
||||
public void CombatChasingPet_KeepsMoveTable()
|
||||
{
|
||||
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||
var target = new PetTestStub();
|
||||
target.MoveToWorld(new Point3D(1003, 1000, 0), Map.Felucca);
|
||||
_created.Add(target);
|
||||
|
||||
pet.SetMoveSpeed(0.3, 0.9);
|
||||
pet.ControlOrder = OrderType.Guard;
|
||||
pet.Combatant = target;
|
||||
pet.SetCurrentSpeedToActive();
|
||||
|
||||
Assert.Equal(0.3, pet.CurrentMoveSpeed);
|
||||
}
|
||||
|
||||
// Herding overrides order pacing.
|
||||
[Fact]
|
||||
public void HerdedObeyingPet_KeepsHerdingPace()
|
||||
{
|
||||
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||
pet.SetMoveSpeed(0.45, 0.9);
|
||||
pet.SetCurrentSpeedToPassive();
|
||||
|
||||
pet.TargetLocation = new Point2D(1010, 1010);
|
||||
|
||||
Assert.Equal(0.3, pet.CurrentMoveSpeed); // fixed herding pace
|
||||
}
|
||||
|
||||
private sealed class ThinkProbe : PetTestStub
|
||||
{
|
||||
public int Thinks;
|
||||
|
|
|
|||
|
|
@ -36,21 +36,6 @@ public class SummonAcquireRulesTests : IDisposable
|
|||
return creature.AIObject.AcquireFocusMob(creature.RangePerception, creature.FightMode, false, false, true);
|
||||
}
|
||||
|
||||
[SkippableTheory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void PreAosFeluccaVortexTurnsOnItsCaster(bool bladeSpirits)
|
||||
{
|
||||
Skip.If(!Server.Tests.TestServerInitializer.TileDataLoaded, "Requires UO client map data.");
|
||||
Core.Expansion = Expansion.UOR;
|
||||
|
||||
var caster = Player(Map.Felucca, 1500, 100);
|
||||
var summon = Summon(bladeSpirits ? new BladeSpirits() : new EnergyVortex(), caster, 1499);
|
||||
|
||||
Assert.True(Acquire(summon));
|
||||
Assert.Same(caster, summon.FocusMob);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void PreAosFeluccaVortexPrefersAnyoneOverItsCaster()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -78,34 +78,6 @@ public class LootingRightsTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HitsMaxBand_MovesTheRightsThreshold()
|
||||
{
|
||||
var victim = new TestMobile();
|
||||
var first = NewPlayer();
|
||||
var second = NewPlayer();
|
||||
|
||||
try
|
||||
{
|
||||
victim.RegisterDamage(100, first);
|
||||
victim.RegisterDamage(40, second);
|
||||
|
||||
// hitsMax >= 200 drops the bar to topDamage / 4 = 31, which 40 clears.
|
||||
var rights = BaseCreature.GetLootingRights(victim.DamageEntries, 200);
|
||||
|
||||
Assert.Equal(2, rights.Count);
|
||||
Assert.True(rights[0].m_HasRight);
|
||||
Assert.True(rights[1].m_HasRight);
|
||||
Assert.Same(second, rights[1].m_Mobile);
|
||||
}
|
||||
finally
|
||||
{
|
||||
victim.Delete();
|
||||
first.Delete();
|
||||
second.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PetDamage_CreditsTheMaster_NotThePet()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Server;
|
||||
using Server.Mobiles;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests.Mobiles;
|
||||
|
||||
// Lord Oaks' abilities and spell shield depend on his queen, so the queen must spawn from combat,
|
||||
// never from a query such as GetMonsterAbilities or a damage calculation.
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class LordOaksTests : IDisposable
|
||||
{
|
||||
private readonly List<Mobile> _created = [];
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var m in World.Mobiles.Values.OfType<Silvani>().ToList())
|
||||
{
|
||||
m.Delete();
|
||||
}
|
||||
|
||||
for (var i = 0; i < _created.Count; i++)
|
||||
{
|
||||
_created[i].Delete();
|
||||
}
|
||||
}
|
||||
|
||||
private LordOaks NewOaks(Map map)
|
||||
{
|
||||
var oaks = new LordOaks();
|
||||
_created.Add(oaks);
|
||||
oaks.MoveToWorld(new Point3D(1000, 1000, 0), map);
|
||||
return oaks;
|
||||
}
|
||||
|
||||
private Mobile NewAttacker()
|
||||
{
|
||||
var attacker = new Mongbat();
|
||||
_created.Add(attacker);
|
||||
return attacker;
|
||||
}
|
||||
|
||||
private static int SilvaniCount() => World.Mobiles.Values.Count(m => m is Silvani);
|
||||
|
||||
[Fact]
|
||||
public void QueryingOrDeletingAnUnengagedOaks_SpawnsNoQueen()
|
||||
{
|
||||
var oaks = NewOaks(Map.Felucca);
|
||||
|
||||
var scalar = 1.0;
|
||||
oaks.AlterDamageScalarFrom(null, ref scalar);
|
||||
oaks.GetMonsterAbilities();
|
||||
oaks.Delete();
|
||||
|
||||
Assert.Equal(1.0, scalar);
|
||||
Assert.Equal(0, SilvaniCount());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aggression_SpawnsTheQueenOnce_AndShieldsOaksWhileSheLives()
|
||||
{
|
||||
var oaks = NewOaks(Map.Felucca);
|
||||
var attacker = NewAttacker();
|
||||
|
||||
oaks.AggressiveAction(attacker, false);
|
||||
oaks.AggressiveAction(attacker, false);
|
||||
|
||||
var queen = World.Mobiles.Values.OfType<Silvani>().Single();
|
||||
|
||||
var shielded = 1.0;
|
||||
oaks.AlterDamageScalarFrom(attacker, ref shielded);
|
||||
Assert.Equal(0.1, shielded, 6);
|
||||
Assert.True(oaks.CanUseAbility(MonsterAbilities.SummonPixiesCounter));
|
||||
|
||||
queen.Delete();
|
||||
|
||||
var exposed = 1.0;
|
||||
oaks.AlterDamageScalarFrom(attacker, ref exposed);
|
||||
oaks.AggressiveAction(attacker, false);
|
||||
|
||||
Assert.Equal(1.0, exposed);
|
||||
Assert.False(oaks.CanUseAbility(MonsterAbilities.SummonPixiesCounter));
|
||||
Assert.Equal(0, SilvaniCount());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnOaksOnTheInternalMap_NeverSpawnsTheQueen()
|
||||
{
|
||||
var oaks = new LordOaks();
|
||||
_created.Add(oaks);
|
||||
|
||||
oaks.AggressiveAction(NewAttacker(), false);
|
||||
|
||||
Assert.Equal(0, SilvaniCount());
|
||||
}
|
||||
}
|
||||
|
|
@ -74,15 +74,6 @@ public class BanExemptionsTests
|
|||
Assert.False(BanExemptions.IsExempt(_listed, "some-future-reason", NeverCalled));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Manual_allowlist_does_not_spend_the_earned_lists_strikes()
|
||||
{
|
||||
WithManualAllowlist("192.0.2.10");
|
||||
|
||||
// Unconditional, so the revocable list must not be consulted -- that would burn a strike.
|
||||
Assert.True(BanExemptions.IsExempt(_listed, BanReasons.RateLimit, NeverCalled));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Falls_through_to_the_login_allowlist_when_not_file_listed()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -59,12 +59,6 @@ public class BlocklistConfigurationTests
|
|||
}
|
||||
|
||||
// The point of the flag: a shard that never opts in must not start the reload poll.
|
||||
[Fact]
|
||||
public void Blocklist_is_off_by_default()
|
||||
{
|
||||
Assert.False(new BlocklistSettings().Enabled);
|
||||
}
|
||||
|
||||
// The generator (tools/Export-IpBlocklist.ps1) writes to this path by default; if one side moves
|
||||
// without the other, a shard silently enforces nothing.
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -72,14 +72,4 @@ public class BlocklistFileTests
|
|||
Assert.True(snap.IsBanned(IPAddress.Parse("223.169.4.9")));
|
||||
File.Delete(p);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_parses_body()
|
||||
{
|
||||
var p = WriteTemp("# generated=x count=1\n8.8.8.0/24\n");
|
||||
var snap = BlocklistFile.Load(p, out var parsed, out _);
|
||||
Assert.Equal(1, parsed);
|
||||
Assert.True(snap.IsBanned(IPAddress.Parse("8.8.8.8")));
|
||||
File.Delete(p);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,12 +54,6 @@ public class BlocklistSnapshotTests
|
|||
Assert.True(s.IsBanned(IPAddress.Parse("5.6.7.200")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_snapshot_matches_nothing()
|
||||
{
|
||||
Assert.False(BlocklistSnapshot.Empty.IsBanned(IPAddress.Parse("1.2.3.4")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ipv6_single_and_cidr_are_matched()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,14 +42,6 @@ public class CrowdSecAlertClientTests
|
|||
Assert.Equal("/v1/decisions?origin=modern%20uo%2Ftest%26x%3D1&ip=1.2.3.4", query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDeleteQuery_PlainOrigin_Ipv4()
|
||||
{
|
||||
var query = CrowdSecAlertClient.BuildDeleteQuery("modernuo", IPAddress.Parse("192.168.1.1"));
|
||||
|
||||
Assert.Equal("/v1/decisions?origin=modernuo&ip=192.168.1.1", query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDeleteQuery_Ipv6_IsEscaped()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -76,14 +76,4 @@ public class FirewallTests
|
|||
Assert.IsType<CidrFirewallEntry>(Server.Network.Firewall.ToFirewallEntry("10.0.0.0-10.0.0.255"));
|
||||
Assert.Null(Server.Network.Firewall.ToFirewallEntry("not-an-ip"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadFirewallSet_SurfacesAddedEntries()
|
||||
{
|
||||
Server.Network.Firewall.ResetForTesting();
|
||||
var entry = new SingleIpFirewallEntry(Ip("1.2.3.4"));
|
||||
Server.Network.Firewall.Add(entry);
|
||||
|
||||
Server.Network.Firewall.ReadFirewallSet(set => Assert.Contains(entry, set));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,12 +50,6 @@ public class ManualAllowlistConfigurationTests
|
|||
}
|
||||
|
||||
// The point of the flag: a shard that never opts in must not start the reload poll.
|
||||
[Fact]
|
||||
public void Manual_allowlist_is_off_by_default()
|
||||
{
|
||||
Assert.False(new ManualAllowlistSettings().Enabled);
|
||||
}
|
||||
|
||||
// The generator creates ip-allowlist.txt beside the blocklist; the wildcard is what picks up a
|
||||
// carve-out file (-RefreshCarveouts writes ip-allowlist-starlink.txt) with no config edit.
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -255,17 +255,6 @@ public class AuthIdTests : IDisposable
|
|||
new ClientVersion(7, 0, 0, 0)
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public void IssuesAnIdWhenTheConnectionHasNone()
|
||||
{
|
||||
var account = CreateAccount("authid-first-select-user");
|
||||
|
||||
var authId = Ensure(0, account, AddressX);
|
||||
|
||||
Assert.NotEqual(0, authId);
|
||||
Assert.Equal(1, IncomingAccountPackets.AuthIdWindowCount);
|
||||
}
|
||||
|
||||
// Handing the same id back rather than minting another is what makes an orphan impossible,
|
||||
// instead of something to clean up afterwards.
|
||||
[Fact]
|
||||
|
|
@ -371,12 +360,4 @@ public class AuthIdTests : IDisposable
|
|||
GameServer.GameServerLoginEvent(right);
|
||||
Assert.True(right.Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratesDistinctAuthIds()
|
||||
{
|
||||
var account = CreateAccount("authid-distinct-user");
|
||||
|
||||
Assert.NotEqual(Register(account, AddressX), Register(account, AddressX));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,68 +141,6 @@ public class DetectHiddenTests
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TryDetectStealther is a no-op when the target is not actually hidden.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryDetectStealther_DoesNothing_WhenStealtherIsNotHidden()
|
||||
{
|
||||
using var rng = new PredictableRandom(10);
|
||||
DetectHidden.ClearDebounceCache();
|
||||
var map = Map.Felucca;
|
||||
var detector = CreatePlayerMobile(map, new Point3D(1800, 500, 0));
|
||||
var stealther = CreatePlayerMobile(map, new Point3D(1801, 500, 0));
|
||||
|
||||
try
|
||||
{
|
||||
detector.Skills.DetectHidden.BaseFixedPoint = 1000;
|
||||
stealther.Hidden = false;
|
||||
|
||||
var result = DetectHidden.TryDetectStealther(detector, stealther);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.False(stealther.Hidden);
|
||||
}
|
||||
finally
|
||||
{
|
||||
detector.Delete();
|
||||
stealther.Delete();
|
||||
DetectHidden.ClearDebounceCache();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Passive detection only works on Felucca. A stealther on Trammel
|
||||
/// should never be revealed by passive detection.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryDetectStealther_DoesNotReveal_WhenNotOnFelucca()
|
||||
{
|
||||
using var rng = new PredictableRandom(10);
|
||||
DetectHidden.ClearDebounceCache();
|
||||
var map = Map.Trammel;
|
||||
var detector = CreatePlayerMobile(map, new Point3D(2000, 500, 0));
|
||||
var stealther = CreatePlayerMobile(map, new Point3D(2001, 500, 0));
|
||||
|
||||
try
|
||||
{
|
||||
detector.Skills.DetectHidden.BaseFixedPoint = 1000; // 100.0
|
||||
stealther.Skills.Hiding.BaseFixedPoint = 0; // 0.0
|
||||
stealther.Hidden = true;
|
||||
|
||||
var result = DetectHidden.TryDetectStealther(detector, stealther);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.True(stealther.Hidden);
|
||||
}
|
||||
finally
|
||||
{
|
||||
detector.Delete();
|
||||
stealther.Delete();
|
||||
DetectHidden.ClearDebounceCache();
|
||||
}
|
||||
}
|
||||
|
||||
private static PlayerMobile CreatePlayerMobile(Map map, Point3D location)
|
||||
{
|
||||
var mobile = new PlayerMobile(World.NewMobile);
|
||||
|
|
|
|||
|
|
@ -81,44 +81,4 @@ public class SkillEventsTests
|
|||
from.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckSkill_Direct_DoesNotRaise()
|
||||
{
|
||||
var from = new Mobile();
|
||||
var skill = from.Skills[SkillName.Mining];
|
||||
var recorder = new Recorder();
|
||||
|
||||
SkillEvents.SkillUsed += recorder.Handle;
|
||||
try
|
||||
{
|
||||
SkillCheck.CheckSkill(from, skill, null, 1.0);
|
||||
Assert.Equal(0, recorder.Calls);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SkillEvents.SkillUsed -= recorder.Handle;
|
||||
from.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoSubscriber_DoesNotThrow()
|
||||
{
|
||||
var from = new Mobile();
|
||||
var recorder = new Recorder();
|
||||
|
||||
try
|
||||
{
|
||||
SkillEvents.SkillUsed += recorder.Handle;
|
||||
SkillEvents.SkillUsed -= recorder.Handle;
|
||||
|
||||
Assert.True(SkillCheck.Mobile_SkillCheckDirectLocation(from, SkillName.Mining, 1.0));
|
||||
Assert.Equal(0, recorder.Calls);
|
||||
}
|
||||
finally
|
||||
{
|
||||
from.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,24 +19,8 @@ public class BloodOathSpellTests
|
|||
// ModernUO previously used /80 (matching the bugged in-game tooltip), which made
|
||||
// Spirit Speak almost irrelevant to duration. RunUO/ServUO and the code's own
|
||||
// fixed-point comment both use /8.
|
||||
[Theory]
|
||||
[InlineData(120.0, 0.0, 23.0)] // GM Spirit Speak, no resist
|
||||
[InlineData(120.0, 120.0, 8.0)] // equal skills -> baseline 8s
|
||||
[InlineData(100.0, 20.0, 18.0)] // (100-20)/8 + 8
|
||||
[InlineData(20.0, 0.0, 10.5)] // minimum skill
|
||||
public void GetDurationSeconds_UsesDivideByEight(double ss, double resist, double expected)
|
||||
{
|
||||
Assert.Equal(expected, BloodOathSpell.GetDurationSeconds(ss, resist), 3);
|
||||
}
|
||||
|
||||
// Player caster: Publish 48 resist mitigation does NOT apply; the attacker takes the
|
||||
// full reflected (original, un-bonused) damage.
|
||||
[Fact]
|
||||
public void ComputeReflectedDamage_NoMitigation_ReturnsOriginal()
|
||||
{
|
||||
Assert.Equal(40, BloodOathSpell.ComputeReflectedDamage(40, 120.0, applyResistMitigation: false));
|
||||
}
|
||||
|
||||
// Creature caster (Publish 48 / SA+): ((Resist * 10) / 20) + 10 = % of reflected damage resisted.
|
||||
[Theory]
|
||||
[InlineData(40, 0.0, 36)] // 10% resisted -> 40 * 0.90
|
||||
|
|
@ -82,14 +66,6 @@ public class BloodOathSpellTests
|
|||
target.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveCurse_NotCursed_ReturnsFalse()
|
||||
{
|
||||
var m = NewMobile();
|
||||
Assert.False(BloodOathSpell.RemoveCurse(m));
|
||||
m.Delete();
|
||||
}
|
||||
|
||||
// Issue #1690: death/delete of either party must break the oath immediately. The oath is wired
|
||||
// to the central PlayerMobile/BaseCreature death+delete events instead of a polling timer.
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -44,17 +44,6 @@ public class AnimalFormTests
|
|||
}
|
||||
|
||||
// Talisman-gated forms are never selectable without the matching talisman, regardless of skill.
|
||||
[Fact]
|
||||
public void CanSelectEntry_TalismanGatedForm_RequiresTalisman()
|
||||
{
|
||||
var m = NewMobileWithNinjitsu(120.0);
|
||||
|
||||
Assert.False(AnimalForm.CanSelectEntry(m, AnimalForm.Entries[IndexOf(typeof(Squirrel))]));
|
||||
Assert.False(AnimalForm.CanSelectEntry(m, AnimalForm.Entries[IndexOf(typeof(Ferret))]));
|
||||
|
||||
m.Delete();
|
||||
}
|
||||
|
||||
// Issue #2452: attempting a form above your skill must report NoSkill (so the caller can
|
||||
// refrain from charging mana) and must not actually transform the caster.
|
||||
[Fact]
|
||||
|
|
@ -70,19 +59,4 @@ public class AnimalFormTests
|
|||
|
||||
m.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Morph_SufficientSkill_TransformsCaster()
|
||||
{
|
||||
// >= ReqSkill + 37.5 guarantees the success-chance roll is skipped.
|
||||
var m = NewMobileWithNinjitsu(60.0);
|
||||
|
||||
var result = AnimalForm.Morph(m, IndexOf(typeof(Rat))); // needs 20
|
||||
|
||||
Assert.Equal(AnimalForm.MorphResult.Success, result);
|
||||
Assert.NotNull(AnimalForm.GetContext(m));
|
||||
|
||||
AnimalForm.RemoveContext(m); // stop the form timer / clear state
|
||||
m.Delete();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,21 +5,6 @@ namespace Server.Tests.Utility;
|
|||
|
||||
public class TryParseTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("True", null, true)]
|
||||
[InlineData("False", null, false)]
|
||||
[InlineData("Alakazam", "Not a valid boolean string.", true)]
|
||||
public void TestTryParseBool(string value, string returned, bool parsedAs)
|
||||
{
|
||||
var actualReturned = Server.Types.TryParse(typeof(bool), value, out var constructed);
|
||||
Assert.Equal(returned, actualReturned);
|
||||
|
||||
if (returned == null)
|
||||
{
|
||||
Assert.Equal(parsedAs, constructed);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// Parsed directly into the target type (INumber<T>.TryParse), not via ulong + Convert.ChangeType.
|
||||
[InlineData(typeof(int), "42", true, 42)]
|
||||
|
|
|
|||
|
|
@ -25,18 +25,6 @@ public class ArchiveJournalTests : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_LoadsCleanState()
|
||||
{
|
||||
// Arrange & Act
|
||||
ArchiveJournal.Configure(_testDir);
|
||||
|
||||
// Assert - fresh directory should have no operations
|
||||
// Note: ArchiveJournal is static, so we check the journal file doesn't exist
|
||||
var journalPath = Path.Combine(_testDir, ".archive-journal.json");
|
||||
Assert.False(File.Exists(journalPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BeginOperation_CreatesStartedEntry()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
using System;
|
||||
using Server.Saves;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class AutoArchiveHelperTests
|
||||
{
|
||||
[Fact]
|
||||
public void ArchiveDestinationRegistry_RegisterAndRetrieve()
|
||||
{
|
||||
// Arrange
|
||||
var dest = new LocalArchiveDestination(24, 30, 12);
|
||||
|
||||
// Act
|
||||
ArchiveDestinationRegistry.Register(dest);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(dest, ArchiveDestinationRegistry.Destinations);
|
||||
|
||||
// Cleanup
|
||||
ArchiveDestinationRegistry.Unregister(dest);
|
||||
Assert.DoesNotContain(dest, ArchiveDestinationRegistry.Destinations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LocalArchiveDestination_SendArchive_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var dest = new LocalArchiveDestination(24, 30, 12);
|
||||
|
||||
// Act
|
||||
var result = dest.SendArchive("/fake/path.tar.zst", ArchivePeriod.Hourly, DateTime.UtcNow);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ArchivePeriod.Hourly, 24)]
|
||||
[InlineData(ArchivePeriod.Daily, 30)]
|
||||
[InlineData(ArchivePeriod.Monthly, 12)]
|
||||
public void LocalArchiveDestination_RetentionCounts(ArchivePeriod period, int expected)
|
||||
{
|
||||
// Arrange
|
||||
var dest = new LocalArchiveDestination(24, 30, 12);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Equal(expected, dest.GetRetentionCount(period));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArchiveCompletedEventArgs_StoresValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
var args = new ArchiveCompletedEventArgs(
|
||||
"/archives/hourly/test.tar.zst",
|
||||
ArchivePeriod.Hourly,
|
||||
new DateTime(2026, 3, 22, 14, 0, 0, DateTimeKind.Utc),
|
||||
1048576,
|
||||
2.5
|
||||
);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/archives/hourly/test.tar.zst", args.ArchiveFilePath);
|
||||
Assert.Equal(ArchivePeriod.Hourly, args.Period);
|
||||
Assert.Equal(1048576, args.FileSizeBytes);
|
||||
Assert.Equal(2.5, args.ElapsedSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArchiveFailedEventArgs_StoresException()
|
||||
{
|
||||
// Arrange
|
||||
var ex = new InvalidOperationException("disk full");
|
||||
|
||||
// Act
|
||||
var args = new ArchiveFailedEventArgs(
|
||||
ArchivePeriod.Daily,
|
||||
new DateTime(2026, 3, 22, 0, 0, 0, DateTimeKind.Utc),
|
||||
ex
|
||||
);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ArchivePeriod.Daily, args.Period);
|
||||
Assert.Same(ex, args.Exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArchiveJournalEntry_DefaultState()
|
||||
{
|
||||
// Arrange & Act
|
||||
var entry = new ArchiveJournalEntry();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(entry.SourceDirectories);
|
||||
Assert.Empty(entry.SourceDirectories);
|
||||
Assert.NotNull(entry.DestinationResults);
|
||||
Assert.Empty(entry.DestinationResults);
|
||||
Assert.Null(entry.FailureReason);
|
||||
Assert.Equal(DateTime.MaxValue, entry.CompletedAt);
|
||||
}
|
||||
}
|
||||
|
|
@ -46,29 +46,6 @@ public class ManagedArchiveTests : IDisposable
|
|||
return dir;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTarZstd_SingleDirectory_RoundTrips()
|
||||
{
|
||||
// Arrange
|
||||
var sourceDir = CreateTestDirectory("source",
|
||||
("file1.txt", "Hello World"),
|
||||
("file2.bin", "Binary content here"),
|
||||
("subdir/nested.txt", "Nested file")
|
||||
);
|
||||
|
||||
var archivePath = Path.Combine(_testDir, "test.tar.zst");
|
||||
|
||||
// Act
|
||||
var entryCount = ManagedArchive.CreateTarZstd(
|
||||
[sourceDir], archivePath, _testDir, compressionLevel: 1
|
||||
);
|
||||
|
||||
// Assert
|
||||
Assert.True(entryCount > 0);
|
||||
Assert.True(File.Exists(archivePath));
|
||||
Assert.True(new FileInfo(archivePath).Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTarZstd_MultipleDirectories_IncludesAll()
|
||||
{
|
||||
|
|
@ -224,30 +201,6 @@ public class ManagedArchiveTests : IDisposable
|
|||
Assert.Equal(-1, count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(3)]
|
||||
[InlineData(10)]
|
||||
public void CreateTarZstd_DifferentCompressionLevels_AllWork(int level)
|
||||
{
|
||||
// Arrange
|
||||
var sourceDir = CreateTestDirectory($"level-{level}", ("data.txt", new string('X', 10000)));
|
||||
var archivePath = Path.Combine(_testDir, $"level-{level}.tar.zst");
|
||||
|
||||
// Act
|
||||
var entryCount = ManagedArchive.CreateTarZstd(
|
||||
[sourceDir], archivePath, _testDir, compressionLevel: level
|
||||
);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, entryCount);
|
||||
Assert.True(File.Exists(archivePath));
|
||||
|
||||
// Verify we can extract it back
|
||||
var extractDir = Path.Combine(_testDir, $"extract-level-{level}");
|
||||
Assert.True(ManagedArchive.ExtractTarZstd(archivePath, extractDir));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractTarGz_RoundTrips()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue