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:
Kamron Batman 2026-10-18 20:08:19 -07:00 • committed by GitHub
parent 078be52777
commit 9a25054809
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 11 additions and 4603 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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("", "")]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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