From 00134cac336922253c2df0c64a2605346938ef67 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Sun, 18 Oct 2026 20:11:33 -0700
Subject: [PATCH] test: fix shared state, CI-vacuous tests and harness
duplication (#2722)
Follow-up to #2721. It fixes how the remaining tests behave and changes no production code.
## CI
- Both `dotnet test` steps pass `--filter "Category!=Manual"`. Before this, Manual-tagged tests ran on CI anyway.
- The one surviving movement-throttle latency test is the only coverage of burst forgiveness over a full, wrapped history. It loses its Manual tag, so it runs on CI.
## Tests that passed on CI without testing anything
- **Corpse and throwing tests.** They relied on tiledata for the weapon `Layer`, so on CI the equip silently failed and they asserted against empty lists. They now set `Layer` explicitly and assert the equip took effect. `TestThrown` also pins its own strength and range values, so a shard retuning SoulGlaive no longer breaks them.
- **Healthbar poison rows.** Server.Tests registers no poisons, so every row tested `null`. They now use real poison levels.
- **Account packet tests.** The mock accounts reported `Length` 0, so the character-list tests only ever encoded empty slots. They now encode the real characters.
- **Pathing and AI tests that only mean something on real Trammel/Felucca terrain** are now guarded with `TileDataRequirement.SkipIfMissing()`: Britain desk and clutter scenes, house footprints (no MultiData on CI) and fingerprints. Tests that are valid on flat blank terrain stay unguarded. (A synthetic tiledata fixture to run these on CI is a planned follow-up.)
- **Assertions that couldn't fail now check their claim.** Affected tests include the `DefaultWalker` cache use, cache on/off parity requiring a path, the recorder capability flags, the idle-pet gate, the monthly DST recurrence, the AdvancedSearch `Record.Exception` wrappers, `CanFitItem` with no branch-guarded assert, the Firewall callback, and `Import_NoMap`. The Geometry Parse asserts also now use the same input as their TryParse.
## Shared state
Every test that changes process-wide state now restores it, even when an assert fails:
- **The clock.** A new `TimerWheelScope` saves and restores `Core._tickCount`, `Core._now` and the timer wheel. `SimulatedClockScope` covers decay tests.
- **StepCache settings and caches**, via `StepCacheStateScope`.
- **Network state.** The ManualAllowlist, LoginAllowlist, firewall and BanChannel reporters are restored. Global packet handlers are now registered per test and removed afterwards, so they no longer stay registered for later tests.
- **Data and config tables.** `ArchiveJournal` statics, `TileData.ItemTable`, `ObjectPropertyList.Enabled`, `Core.Expansion` and the localization table are restored. One `MobileStatusVersion` restore was resetting a different expansion than the one the test changed.
- **Entities.** Tests with fixed serials now delete what they create. Fixed mobile serials moved to `World.NewMobile`, because deleting a hard-coded serial could unregister a live entity.
- **Collection race.** `SpiralScanTests` no longer clears the spawn cache from a separate collection that could run in parallel with the cache-manager tests.
- **Shared statics.** Classes reading shared static allowlists joined the sequential collection.
## Duplication
New shared helpers replace copy-pasted harness code:
- **Server.Tests:** `PacketAssert.Sends`, used by about 130 packet tests; `TestClientHarness`, `PersistencePipelineScope`, `NetTestHelpers`, `EntityCleanup` and one `TestMulti`.
- **UOContent.Tests:** `TimerWheelScope`, which also holds one wraparound-safe `RunFor`/`RunUntil`; `AiTestStubs`, which merges six walker copies and five identical creature stubs; `MobileTestHarness`, `CommandTestHarness` and `TempDirectory`.
Dead oracle packet classes are removed.
## Cost
| Test | Before | After |
|---|---|---|
| `VirtualSerialTests` | 1.57s | 2ms (starts near the wrap via reflection instead of drawing ~17.9M serials) |
| Event scheduler recurrence loop | 473ms | 1ms |
| CrowdSec drain | 517ms | 16ms (event-driven instead of a fixed 500ms wait) |
| AuthId tests | 248ms | 99ms (SHA2 except the two GameLogin tests) |
| Container cap tests | 5,000 items | 3,449 / 3,277 (one past each protocol cap) |
| Bulk removal | 100k children | 20k children |
| OPL hash distribution | 60k builds | 10k builds |
## Shard-tuning decoupling
- AI assertions read `ReacquireDelay` and `FollowMoveSpeed` instead of hard-coding production tuning.
- The guard friendly-fire test uses an `AI_Mage` stub instead of WhiteWyrm.
- Argon2 assertions pin the `argon2id` variant and `v=19` but read the cost parameters from the configured hasher.
## Tests
| Run | Server.Tests | UOContent.Tests |
|---|---|---|
| With client files | 812 pass | 1,123 pass |
| Without client files (CI simulation) | 800 pass, 12 skip | 1,021 pass, 102 skip |
- The UOContent fixture reads `MODERNUO_TEST_DATA_DIR`; Server.Tests reads `MODERNUO_CLIENT_PATH`.
- The 15 new UOContent skips are the map-dependent guards above.
- Total run time with client files is about 5s, down from about 8s.
---
.github/workflows/build-test.yml | 4 +-
.../Server.Tests/Helpers/EntityCleanup.cs | 33 ++
Projects/Server.Tests/Helpers/MockAccount.cs | 18 +-
.../Server.Tests/Helpers/NetTestHelpers.cs | 152 ++++++++
Projects/Server.Tests/Helpers/PacketAssert.cs | 31 ++
.../Helpers/PersistencePipelineScope.cs | 130 +++++++
.../Helpers/SimulatedClockScope.cs | 42 ++
.../Server.Tests/Helpers/TestClientHarness.cs | 82 ++++
Projects/Server.Tests/Helpers/TestMulti.cs | 17 +
.../Tests/Client/ClientVersionTests.cs | 3 +-
.../Tests/Console/ConsoleInputPumpTests.cs | 12 +-
.../Tests/Geometry/Point3DTests.cs | 2 +-
.../Tests/Geometry/Rectangle3DTests.cs | 2 +-
.../Tests/Geometry/WorldLocationTests.cs | 6 +-
.../Tests/Items/ContainerBulkRemovalTests.cs | 78 ++--
.../Tests/Items/ContainerChildRemovalTests.cs | 54 +--
.../Tests/Items/DecayRegistrationTests.cs | 299 ++++++--------
.../Tests/Items/LiftRejectResyncTests.cs | 49 +--
.../Items/LockedDownContainerDecayTests.cs | 48 +--
.../Items/PrivateChildPropertiesTests.cs | 60 +--
.../Localization/LocalizationEntryTests.cs | 20 +-
.../Tests/Maps/CanFitItemTests.cs | 32 +-
.../Tests/Maps/CanSpawnMobileTests.cs | 16 +-
.../Tests/Maps/CanSpawnMobileTileDataTests.cs | 29 +-
.../Tests/Maps/MultiEnumeratorTests.cs | 34 +-
.../Tests/Maps/StaticTileEnumeratorTests.cs | 73 ++--
.../Tests/Network/Bans/BanChannelTests.cs | 27 +-
.../Network/MovementThrottleLatencyTests.cs | 93 +----
.../Tests/Network/NetStateDisconnectTests.cs | 62 +--
.../Tests/Network/NetStateRecvGuardTests.cs | 98 ++---
.../Tests/Network/NetStateSendBufferTests.cs | 142 ++-----
.../Packets/Outgoing/AccountPacketTests.cs | 203 +++-------
.../Packets/Outgoing/CombatPacketTests.cs | 24 +-
.../Packets/Outgoing/ContainerPacketTests.cs | 286 +++++---------
.../Packets/Outgoing/DamagePacketTests.cs | 22 +-
.../Packets/Outgoing/EffectPacketTests.cs | 23 +-
.../Network/Packets/Outgoing/EffectPackets.cs | 151 --------
.../Packets/Outgoing/EquipmentPacketTests.cs | 29 +-
.../Packets/Outgoing/GumpPacketTests.cs | 41 +-
.../Packets/Outgoing/ItemPacketTests.cs | 91 ++---
.../Network/Packets/Outgoing/ItemPackets.cs | 18 -
.../Packets/Outgoing/LightPacketTests.cs | 17 +-
.../Packets/Outgoing/MapPacketTests.cs | 24 +-
.../Packets/Outgoing/MenuPacketTests.cs | 33 +-
.../Packets/Outgoing/MessagePacketTests.cs | 196 ++--------
.../Packets/Outgoing/MobilePacketTests.cs | 365 +++++-------------
.../Packets/Outgoing/MovementPacketTests.cs | 34 +-
.../Packets/Outgoing/PlayerPacketTests.cs | 224 +++--------
.../Outgoing/SecureTradePacketTests.cs | 61 +--
.../Packets/Outgoing/TargetPacketsTests.cs | 31 +-
.../Packets/Outgoing/VendorBuyPacketTests.cs | 44 +--
.../Packets/Outgoing/VendorSellPacketTests.cs | 19 +-
.../Outgoing/VirtualHairPacketTests.cs | 19 +-
.../Packets/Outgoing/VirtualHairPackets.cs | 25 --
.../ObjectPropertyListHashTests.cs | 35 +-
.../Tests/Serialization/AnchoredTimeTests.cs | 103 ++---
.../GenericEntityPersistenceRoundTripTests.cs | 293 +++++---------
.../SerializationChunkSourceTests.cs | 163 ++++----
.../Tests/World/VirtualSerialTests.cs | 41 +-
.../UOContent.Tests/Helpers/AiTestStubs.cs | 42 ++
.../Helpers/CommandTestHarness.cs | 40 ++
.../Helpers/MobileTestHarness.cs | 114 ++++++
.../Helpers/StepCacheStateScope.cs | 45 +++
.../UOContent.Tests/Helpers/TempDirectory.cs | 44 +++
.../Helpers/TimerWheelScope.cs | 140 +++++++
.../Tests/Accounting/AccountPasswordTests.cs | 5 +-
.../Tests/Accounting/PasswordWorkerTests.cs | 26 +-
.../Security/Argon2TestParameters.cs | 55 +++
.../Security/PasswordProtectionTest.cs | 64 ++-
.../Tests/Commands/ChainedBindingSortTests.cs | 29 +-
.../Commands/ConditionalCompilerEdgeTests.cs | 35 +-
.../Tests/Commands/DistinctComparerTests.cs | 19 +-
.../Tests/Commands/EmitterRobustnessTests.cs | 6 +-
.../Tests/Commands/ObjectConditionalTests.cs | 24 +-
.../AdvancedSearchConditionsTests.cs | 6 +-
.../AdvancedSearchTypesTests.cs | 11 +-
.../Engines/Events/EventSchedulerTests.cs | 58 +--
.../Engines/Events/RecurrencePatternTests.cs | 26 +-
.../Tests/Engines/Harvest/FishingTests.cs | 18 +-
.../Tests/Engines/Party/PartyPacketTests.cs | 65 ++--
.../Pathing/BitmapAStarAlgorithmTests.cs | 81 ++--
.../Pathing/Multi/HousePathRoutingTests.cs | 13 +-
.../Pathing/Multi/MultiCacheUsedTests.cs | 13 +-
.../Pathing/Multi/MultiMaskCacheTests.cs | 15 +-
.../Pathing/Multi/MultiPathInvariantTests.cs | 44 +--
.../Pathing/Multi/MultiSplitRoutingTests.cs | 28 +-
.../Engines/Pathing/Multi/MultiTestSupport.cs | 6 -
.../Pathing/Multi/MultiWalkabilityTests.cs | 5 -
.../Engines/Pathing/PathfindRecorderTests.cs | 19 +-
.../Engines/Pathing/PathingTestSupport.cs | 17 +-
.../Engines/Pathing/StepCacheFileTests.cs | 29 +-
.../Pathing/StepCacheFingerprintTests.cs | 5 +-
.../Pathing/StepCacheLifecycleTests.cs | 37 +-
.../Engines/Pathing/StepCacheParityTests.cs | 19 +-
.../Spawners/Json/AllSpawnFilesLoadTests.cs | 8 +-
.../Spawners/Json/ImportCleanupTests.cs | 79 ++--
.../Spawners/Json/SpawnerDtoEntryTests.cs | 33 +-
.../Engines/Spawners/SectorSpawnCacheTests.cs | 6 -
.../Spawners/SpawnerEntryOwnershipTests.cs | 62 +--
.../Misc/Corpses/CorpseChildPublicTests.cs | 167 ++++----
.../Items/Misc/Corpses/CorpseLootTests.cs | 27 +-
.../Items/Misc/Corpses/CorpsePacketTests.cs | 53 ++-
.../Tests/Items/VendorBuybackPackTests.cs | 12 +-
.../Items/Weapons/Throwing/ThrowingTests.cs | 36 +-
.../Tests/Misc/CorpseNotorietyTests.cs | 17 +-
.../UOContent.Tests/Tests/Misc/EmailTests.cs | 19 +-
.../Tests/Misc/GuildSerializePurityTests.cs | 57 ++-
.../Tests/Misc/NameVerificationTests.cs | 2 +-
.../Tests/Mobiles/AI/AcquisitionTests.cs | 22 +-
.../Tests/Mobiles/AI/ApproachOutcomeTests.cs | 98 ++---
.../Tests/Mobiles/AI/ApproachTargetTests.cs | 124 +++---
.../Mobiles/AI/DetectHiddenTimerTests.cs | 29 +-
.../Tests/Mobiles/AI/FamiliarAITests.cs | 40 +-
.../Tests/Mobiles/AI/GuardFollowTests.cs | 46 ++-
.../Mobiles/AI/GuardFriendlyFireTests.cs | 44 ++-
.../Tests/Mobiles/AI/GuardOrderTests.cs | 22 +-
.../Tests/Mobiles/AI/HerdingTests.cs | 7 +-
.../Tests/Mobiles/AI/PetOrderTests.cs | 16 +-
.../Tests/Mobiles/AI/PetPacingTests.cs | 50 +--
.../Tests/Mobiles/AI/PetRetaliationTests.cs | 10 +-
.../Tests/Mobiles/AI/PetTestStub.cs | 12 +-
.../Mobiles/BaseCreatureSerializationTests.cs | 110 +-----
.../Tests/Mobiles/DeletedMasterTests.cs | 37 +-
.../Tests/Mobiles/MasterViewsTests.cs | 37 +-
.../Tests/Mobiles/VendorBuybackTests.cs | 129 ++-----
.../Tests/Multis/Houses/HousePackets.cs | 6 +-
.../Tests/Network/BanExemptionsTests.cs | 16 +-
.../Bans/Blocklist/BlocklistFileTests.cs | 17 +-
.../Bans/Blocklist/BlocklistFilterTests.cs | 6 +-
.../Network/Bans/CrowdSecReporterTests.cs | 34 +-
.../Firewall/FirewallPersistenceTests.cs | 5 +-
.../Tests/Network/Firewall/FirewallTests.cs | 5 +-
.../LoginAllowlist/LoginAllowlistTests.cs | 63 ++-
.../Tests/Network/Packets/AuthIdTests.cs | 38 +-
.../Tests/WorldSaves/ArchiveJournalTests.cs | 17 +
135 files changed, 3164 insertions(+), 4020 deletions(-)
create mode 100644 Projects/Server.Tests/Helpers/EntityCleanup.cs
create mode 100644 Projects/Server.Tests/Helpers/NetTestHelpers.cs
create mode 100644 Projects/Server.Tests/Helpers/PacketAssert.cs
create mode 100644 Projects/Server.Tests/Helpers/PersistencePipelineScope.cs
create mode 100644 Projects/Server.Tests/Helpers/SimulatedClockScope.cs
create mode 100644 Projects/Server.Tests/Helpers/TestClientHarness.cs
create mode 100644 Projects/Server.Tests/Helpers/TestMulti.cs
create mode 100644 Projects/UOContent.Tests/Helpers/AiTestStubs.cs
create mode 100644 Projects/UOContent.Tests/Helpers/CommandTestHarness.cs
create mode 100644 Projects/UOContent.Tests/Helpers/MobileTestHarness.cs
create mode 100644 Projects/UOContent.Tests/Helpers/StepCacheStateScope.cs
create mode 100644 Projects/UOContent.Tests/Helpers/TempDirectory.cs
create mode 100644 Projects/UOContent.Tests/Helpers/TimerWheelScope.cs
create mode 100644 Projects/UOContent.Tests/Tests/Accounting/Security/Argon2TestParameters.cs
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index eb74c39a4..d9219a87f 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -57,7 +57,7 @@ jobs:
# blame-hang kills a stuck test host after 10 minutes and reports the in-flight
# tests plus a process dump instead of hanging until the job timeout.
run: |
- dotnet test --logger trx --results-directory ./TestResults --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type full
+ dotnet test --filter "Category!=Manual" --logger trx --results-directory ./TestResults --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type full
if [ -z "$(find ./TestResults -name '*.trx' 2>/dev/null)" ]; then
echo "::error::No test result files were produced - no test projects ran. Failing to avoid masking failures."
exit 1
@@ -167,7 +167,7 @@ jobs:
# blame-hang kills a stuck test host after 10 minutes and reports the in-flight
# tests plus a process dump instead of hanging until the job timeout.
run: |
- dotnet test --logger trx --results-directory ./TestResults --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type full
+ dotnet test --filter "Category!=Manual" --logger trx --results-directory ./TestResults --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type full
if [ -z "$(find ./TestResults -name '*.trx' 2>/dev/null)" ]; then
echo "::error::No test result files were produced - no test projects ran. Failing to avoid masking failures."
exit 1
diff --git a/Projects/Server.Tests/Helpers/EntityCleanup.cs b/Projects/Server.Tests/Helpers/EntityCleanup.cs
new file mode 100644
index 000000000..61917b61e
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/EntityCleanup.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+
+namespace Server.Tests;
+
+///
+/// Deletes tracked entities on dispose, so a test that fails mid-way does not leave them in the shared World.
+/// Null and already-deleted entities are skipped.
+///
+internal sealed class EntityCleanup : IDisposable
+{
+ private readonly List _entities = [];
+
+ public T Track(T entity) where T : IEntity
+ {
+ _entities.Add(entity);
+ return entity;
+ }
+
+ public void Dispose()
+ {
+ // Reverse order deletes children before the containers or owners that hold them.
+ for (var i = _entities.Count - 1; i >= 0; i--)
+ {
+ if (_entities[i] is { Deleted: false } entity)
+ {
+ entity.Delete();
+ }
+ }
+
+ _entities.Clear();
+ }
+}
diff --git a/Projects/Server.Tests/Helpers/MockAccount.cs b/Projects/Server.Tests/Helpers/MockAccount.cs
index 8d8973d86..2725d3438 100644
--- a/Projects/Server.Tests/Helpers/MockAccount.cs
+++ b/Projects/Server.Tests/Helpers/MockAccount.cs
@@ -20,15 +20,27 @@ public class MockAccount : IAccount
public string Username { get; }
public string Email { get; set; }
public AccessLevel AccessLevel { get; set; }
- public int Length { get; }
+ public int Length { get; private set; }
public int Limit { get; set; } = 6; // Default to 6 character slots
- public int Count { get; }
+ public int Count { get; private set; }
private readonly Dictionary _mobiles = new();
public Mobile this[int index]
{
get => _mobiles.GetValueOrDefault(index);
- set => _mobiles[index] = value;
+ set
+ {
+ var old = _mobiles.GetValueOrDefault(index);
+ _mobiles[index] = value;
+
+ Length = Math.Max(Length, index + 1);
+ Count += (value != null ? 1 : 0) - (old != null ? 1 : 0);
+
+ if (value != null)
+ {
+ value.Account = this;
+ }
+ }
}
public DateTime Created { get; set; }
diff --git a/Projects/Server.Tests/Helpers/NetTestHelpers.cs b/Projects/Server.Tests/Helpers/NetTestHelpers.cs
new file mode 100644
index 000000000..3f3cef3fc
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/NetTestHelpers.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Net.Sockets;
+using System.Network;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using Server.Network;
+using Xunit;
+
+namespace Server.Tests.Network;
+
+///
+/// Shared plumbing for tests that drive a real over loopback.
+///
+public static class NetTestHelpers
+{
+ public const byte PingPacketId = 0x73;
+
+ /// The payload byte of the last ping delivered to the handler from .
+ public static byte LastPing { get; private set; }
+
+ ///
+ /// Restores the handler that was in a packet slot before a test replaced it. The handler table is
+ /// process-global, so a handler left behind would silently answer for that id in every later test.
+ ///
+ public readonly struct HandlerRegistration : IDisposable
+ {
+ private readonly int _packetId;
+ private readonly PacketHandler _previous;
+
+ internal HandlerRegistration(int packetId, PacketHandler previous)
+ {
+ _packetId = packetId;
+ _previous = previous;
+ }
+
+ public void Dispose() => IncomingPackets.Handlers[_packetId] = _previous;
+ }
+
+ public static unsafe HandlerRegistration RegisterHandler(
+ int packetId, int length, delegate* onReceive
+ )
+ {
+ var previous = IncomingPackets.GetHandler(packetId);
+ IncomingPackets.Register(packetId, length, false, onReceive);
+ return new HandlerRegistration(packetId, previous);
+ }
+
+ /// Registers a two-byte ping whose payload byte is recorded in .
+ public static unsafe HandlerRegistration RegisterPing()
+ {
+ LastPing = 0;
+ return RegisterHandler(PingPacketId, 2, &RecordPing);
+ }
+
+ private static void RecordPing(NetState state, SpanReader reader) => LastPing = reader.ReadByte();
+
+ public static byte[] Pattern(int length, int seed)
+ {
+ var data = new byte[length];
+ new System.Random(seed).NextBytes(data);
+ return data;
+ }
+
+ ///
+ /// Pre-auth buffers are off only where the platform floor leaves no room below the base size
+ /// (the Windows Server 2012/2016 mapping path, or a 64 KiB-page kernel). Anywhere else a zero
+ /// initial size is a configuration regression and fails rather than skips.
+ ///
+ public static void RequirePreAuthBuffers()
+ {
+ var floor = IORingBuffer.MinimumSize;
+ Skip.If(
+ floor >= NetState.RecvBufferSize || floor >= NetState.SendBufferSize,
+ $"Pre-auth buffers are off on this platform: the {floor}-byte mapping floor leaves no room below the base buffers (recv {NetState.RecvBufferSize}, send {NetState.SendBufferSize})"
+ );
+
+ Assert.True(
+ NetState.InitialRecvBufferSize > 0 && NetState.InitialSendBufferSize > 0,
+ $"Pre-auth buffers are off although the {floor}-byte platform floor allows them (initial recv {NetState.InitialRecvBufferSize}, send {NetState.InitialSendBufferSize})"
+ );
+ }
+
+ /// Runs the network slice until holds, failing with the condition that timed out.
+ public static void SliceUntil(
+ Func done, int timeoutMs = 5000, [CallerArgumentExpression(nameof(done))] string condition = null
+ )
+ {
+ var deadline = Stopwatch.StartNew();
+ while (!done())
+ {
+ if (deadline.ElapsedMilliseconds >= timeoutMs)
+ {
+ Assert.Fail($"Timed out after {timeoutMs} ms waiting for {condition}");
+ }
+
+ NetState.Slice();
+ Thread.Sleep(5);
+ }
+ }
+
+ /// Reads exactly bytes from the peer, slicing the server while it waits.
+ public static byte[] ReadAll(Socket client, int length, int timeoutMs = 10000)
+ {
+ var received = new byte[length];
+ var total = 0;
+ var deadline = Stopwatch.StartNew();
+ while (total < length)
+ {
+ if (deadline.ElapsedMilliseconds >= timeoutMs)
+ {
+ Assert.Fail($"Timed out after {timeoutMs} ms with {total} of {length} bytes received by the peer");
+ }
+
+ NetState.Slice();
+
+ // Poll takes microseconds, not milliseconds
+ if (client.Poll(50_000, SelectMode.SelectRead))
+ {
+ var read = client.Receive(received, total, length - total, SocketFlags.None);
+ Assert.True(read != 0, $"Peer saw EOF after {total} of {length} bytes");
+ total += read;
+ }
+ }
+
+ return received;
+ }
+
+ /// Waits for the server's FIN to reach the peer, failing if any data arrives first.
+ public static void WaitForEof(Socket client, int timeoutMs = 10000)
+ {
+ var scratch = new byte[64];
+ var deadline = Stopwatch.StartNew();
+ while (true)
+ {
+ if (deadline.ElapsedMilliseconds >= timeoutMs)
+ {
+ Assert.Fail($"Timed out after {timeoutMs} ms waiting for the peer to see EOF");
+ }
+
+ NetState.Slice();
+
+ if (client.Poll(50_000, SelectMode.SelectRead))
+ {
+ var read = client.Receive(scratch);
+ Assert.True(read == 0, $"Peer received {read} unexpected bytes while waiting for EOF");
+ return;
+ }
+ }
+ }
+}
diff --git a/Projects/Server.Tests/Helpers/PacketAssert.cs b/Projects/Server.Tests/Helpers/PacketAssert.cs
new file mode 100644
index 000000000..ca8efb5ee
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/PacketAssert.cs
@@ -0,0 +1,31 @@
+using System;
+using Server.Network;
+
+namespace Server.Tests.Network;
+
+public static class PacketAssert
+{
+ ///
+ /// Asserts that writes exactly the bytes of the reference
+ /// packet to a fresh test NetState negotiated with .
+ ///
+ public static void Sends(Packet expected, Action send, ProtocolChanges changes = ProtocolChanges.None) =>
+ Sends(_ => expected, send, changes);
+
+ ///
+ /// As , for a reference packet that depends on
+ /// the negotiated NetState. It is built after is applied and before the send.
+ ///
+ public static void Sends(
+ Func expected, Action send, ProtocolChanges changes = ProtocolChanges.None
+ )
+ {
+ using var ns = PacketTestUtilities.CreateTestNetState();
+ ns.ProtocolChanges = changes;
+
+ var expectedBytes = expected(ns).Compile();
+ send(ns);
+
+ AssertThat.Equal(ns.SendBuffer.GetReadSpan(), expectedBytes);
+ }
+}
diff --git a/Projects/Server.Tests/Helpers/PersistencePipelineScope.cs b/Projects/Server.Tests/Helpers/PersistencePipelineScope.cs
new file mode 100644
index 000000000..dbdb05524
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/PersistencePipelineScope.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+
+namespace Server.Tests;
+
+///
+/// A private save pipeline: a chunk source with its own serialization workers, optionally installed as
+/// World's workers, optionally a scratch directory and an extra assembly visible to the type loader.
+/// Dispose unregisters every tracked persistence, exits the workers and restores every static it swapped.
+///
+internal sealed class PersistencePipelineScope : IDisposable
+{
+ private readonly List _persistences = [];
+ private readonly SerializationThreadWorker[] _previousWorkers;
+ private readonly Assembly[] _previousAssemblies;
+ private readonly DateTime _previousSaveStart;
+ private readonly bool _installedWorkers;
+ private readonly bool _swappedAssemblies;
+
+ /// Workers to create, each with an allocated heap.
+ /// Install the workers as World's, which WriteSnapshot reads segments from.
+ /// An assembly the loader must resolve types from by hash.
+ /// Creates a unique scratch directory under the temp path when set.
+ public PersistencePipelineScope(
+ int workerCount,
+ bool installWorkers = false,
+ Assembly visibleAssembly = null,
+ string directoryPrefix = null
+ )
+ {
+ _previousSaveStart = World.SaveStartTime;
+
+ Source = new SerializationChunkSource();
+ Workers = new SerializationThreadWorker[workerCount];
+ for (var i = 0; i < Workers.Length; i++)
+ {
+ Workers[i] = new SerializationThreadWorker(i, Source);
+ Workers[i].AllocateHeap();
+ }
+
+ if (installWorkers)
+ {
+ _installedWorkers = true;
+ _previousWorkers = World._threadWorkers;
+ World._threadWorkers = Workers;
+ }
+
+ if (visibleAssembly != null)
+ {
+ _swappedAssemblies = true;
+ _previousAssemblies = AssemblyHandler.Assemblies;
+ AssemblyHandler.Assemblies = [.. _previousAssemblies ?? [], visibleAssembly];
+ }
+
+ if (directoryPrefix != null)
+ {
+ Directory = Path.Combine(Path.GetTempPath(), $"{directoryPrefix}-{Guid.NewGuid():N}");
+ System.IO.Directory.CreateDirectory(Directory);
+ }
+ }
+
+ public SerializationChunkSource Source { get; }
+
+ public SerializationThreadWorker[] Workers { get; }
+
+ /// The scratch directory, or null when none was requested.
+ public string Directory { get; }
+
+ /// Tracks a persistence so it leaves the static registry on dispose.
+ public T Track(T persistence) where T : Persistence
+ {
+ _persistences.Add(persistence);
+ return persistence;
+ }
+
+ public void WakeWorkers()
+ {
+ for (var i = 0; i < Workers.Length; i++)
+ {
+ Workers[i].Wake();
+ }
+ }
+
+ public void SleepWorkers()
+ {
+ for (var i = 0; i < Workers.Length; i++)
+ {
+ Workers[i].Sleep();
+ }
+ }
+
+ /// Mirrors World.PauseSerializationThreads: publish the partial chunk, then wait for the drain.
+ public void FlushAndSleepWorkers()
+ {
+ Source.Flush();
+ SleepWorkers();
+ }
+
+ public void Dispose()
+ {
+ World.SaveStartTime = _previousSaveStart;
+
+ for (var i = 0; i < _persistences.Count; i++)
+ {
+ _persistences[i]?.Unregister();
+ }
+
+ for (var i = 0; i < Workers.Length; i++)
+ {
+ Workers[i].Exit();
+ }
+
+ if (_installedWorkers)
+ {
+ World._threadWorkers = _previousWorkers;
+ }
+
+ if (_swappedAssemblies)
+ {
+ AssemblyHandler.Assemblies = _previousAssemblies;
+ }
+
+ if (Directory != null)
+ {
+ System.IO.Directory.Delete(Directory, true);
+ }
+ }
+}
diff --git a/Projects/Server.Tests/Helpers/SimulatedClockScope.cs b/Projects/Server.Tests/Helpers/SimulatedClockScope.cs
new file mode 100644
index 000000000..cb4b5acfd
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/SimulatedClockScope.cs
@@ -0,0 +1,42 @@
+using System;
+using Server.Items;
+
+namespace Server.Tests;
+
+///
+/// Lets a test move the game clock () and restores it on dispose, so a failed
+/// assert cannot leave every later test running in the future.
+///
+internal sealed class SimulatedClockScope : IDisposable
+{
+ private static readonly TimeSpan _decayTickStep = TimeSpan.FromMilliseconds(256);
+
+ public SimulatedClockScope() => Start = Core._now;
+
+ public DateTime Start { get; }
+
+ public DateTime Now
+ {
+ get => Core._now;
+ set => Core._now = value;
+ }
+
+ ///
+ /// Steps the clock from through , driving the decay
+ /// scheduler each step without the shared timer wheel, and stops early once
+ /// is deleted.
+ ///
+ public void AdvanceDecay(TimeSpan duration, Item item)
+ {
+ var start = Core._now;
+ var deadline = start + duration;
+
+ for (var now = start; now <= deadline && !item.Deleted; now += _decayTickStep)
+ {
+ Core._now = now;
+ DecayScheduler.ProcessTick(now);
+ }
+ }
+
+ public void Dispose() => Core._now = Start;
+}
diff --git a/Projects/Server.Tests/Helpers/TestClientHarness.cs b/Projects/Server.Tests/Helpers/TestClientHarness.cs
new file mode 100644
index 000000000..aa9dde5d0
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/TestClientHarness.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Buffers;
+using Server.Network;
+using Server.Tests.Network;
+
+namespace Server.Tests;
+
+///
+/// A logged-in test client (NetState + Mobile) and byte-pattern probes over what it was sent.
+/// The probes search the raw send buffer, so they only see packets queued since the NetState was created.
+///
+internal static class TestClientHarness
+{
+ public static (NetState, Mobile) CreateClient(Point3D location)
+ {
+ var ns = PacketTestUtilities.CreateTestNetState();
+ ns.Account = new MockAccount();
+
+ var mobile = new Mobile(World.NewMobile);
+ mobile.DefaultMobileInit();
+ ns.Mobile = mobile;
+ mobile.NetState = ns;
+ mobile.MoveToWorld(location, Map.Felucca);
+ return (ns, mobile);
+ }
+
+ public static void DisposeClient(NetState ns, Mobile mobile)
+ {
+ ns.Mobile = null;
+ ns.Dispose();
+ mobile.Delete();
+ }
+
+ public static bool ReceivedRemove(NetState ns, Serial serial) => CountRemoves(ns, serial) > 0;
+
+ public static int CountRemoves(NetState ns, Serial serial)
+ {
+ Span expected = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength];
+ OutgoingEntityPackets.CreateRemoveEntity(expected, serial);
+
+ var span = ns.SendBuffer.GetReadSpan();
+ var count = 0;
+
+ while (true)
+ {
+ var index = span.IndexOf(expected);
+
+ if (index < 0)
+ {
+ break;
+ }
+
+ count++;
+ span = span[(index + expected.Length)..];
+ }
+
+ return count;
+ }
+
+ public static bool ReceivedWorldItem(NetState ns, Item item)
+ {
+ Span expected = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength];
+ var length = OutgoingItemPackets.CreateWorldItem(expected, item);
+ return ns.SendBuffer.GetReadSpan().IndexOf(expected[..length]) >= 0;
+ }
+
+ public static bool ReceivedContentUpdate(NetState ns, Serial serial) => ReceivedPacketForSerial(ns, 0x25, serial);
+
+ public static bool ReceivedEquipUpdate(NetState ns, Serial serial) => ReceivedPacketForSerial(ns, 0x2E, serial);
+
+ public static bool ReceivedOplInfo(NetState ns, Serial serial) => ReceivedPacketForSerial(ns, 0xDC, serial);
+
+ // Each of these packets is fixed-length with the serial immediately after the packet ID.
+ private static bool ReceivedPacketForSerial(NetState ns, byte packetId, Serial serial)
+ {
+ Span expected = stackalloc byte[5];
+ var writer = new SpanWriter(expected);
+ writer.Write(packetId);
+ writer.Write(serial);
+ return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
+ }
+}
diff --git a/Projects/Server.Tests/Helpers/TestMulti.cs b/Projects/Server.Tests/Helpers/TestMulti.cs
new file mode 100644
index 000000000..b7363c3f7
--- /dev/null
+++ b/Projects/Server.Tests/Helpers/TestMulti.cs
@@ -0,0 +1,17 @@
+using Server.Items;
+
+namespace Server.Tests;
+
+///
+/// A multi with caller-supplied components. The test host never loads MultiData, so a multi built
+/// from client data would have no components; without a list this one is empty the same way.
+///
+internal sealed class TestMulti : BaseMulti
+{
+ private readonly MultiComponentList _components;
+
+ public TestMulti(MultiComponentList components = null) : base(0x1) =>
+ _components = components ?? MultiComponentList.Empty;
+
+ public override MultiComponentList Components => _components;
+}
diff --git a/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs b/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs
index 471a5584c..39a73ad7b 100644
--- a/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs
+++ b/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs
@@ -2,13 +2,13 @@ using Xunit;
namespace Server.Tests.Network;
-[Collection("Sequential Server Tests")]
public class ClientVersionTests
{
[Theory]
[InlineData("7.0.33.1", "67.0.33", false, 0)]
[InlineData("7.0.45.65", "67.0.33", false, 1)]
[InlineData("6.0.0.0", "67.0.33", false, -1)]
+ [InlineData("7.0.33.1", "7.0.33.1", true, 0)]
public void TestCCAndECStringCtor(string ccVersion, string ecVersion, bool equal, int comparison)
{
var cc = new ClientVersion(ccVersion);
@@ -22,6 +22,7 @@ public class ClientVersionTests
[InlineData(7, 0, 33, 1, 67, 0, 33, 0, false, 0)]
[InlineData(7, 0, 45, 65, 67, 0, 33, 0, false, 1)]
[InlineData(6, 0, 0, 0, 67, 0, 33, 0, false, -1)]
+ [InlineData(7, 0, 33, 1, 7, 0, 33, 1, true, 0)]
public void TestCCAndECCtor(
int ccMaj, int ccMin, int ccRev, int ccPatch, int ecMaj, int ecMin, int ecRev, int ecPatch,
bool equal, int comparison
diff --git a/Projects/Server.Tests/Tests/Console/ConsoleInputPumpTests.cs b/Projects/Server.Tests/Tests/Console/ConsoleInputPumpTests.cs
index 50d015555..cdf084fb7 100644
--- a/Projects/Server.Tests/Tests/Console/ConsoleInputPumpTests.cs
+++ b/Projects/Server.Tests/Tests/Console/ConsoleInputPumpTests.cs
@@ -70,10 +70,11 @@ public class ConsoleInputPumpTests
cmd => cmd == "ping" ? onPing : null
);
- Task.Run(pump.Run);
+ var run = Task.Run(pump.Run);
Assert.True(invokedWith.Task.Wait(Timeout), "command not dispatched");
Assert.Equal("hello", invokedWith.Task.Result);
+ Assert.True(run.Wait(Timeout), "Run() did not return on EOF");
}
[Fact]
@@ -81,7 +82,7 @@ public class ConsoleInputPumpTests
{
var reader = new BlockingTextReader();
var pump = new ConsoleInputPump(reader, _ => null);
- Task.Run(pump.Run);
+ var run = Task.Run(pump.Run);
// Register the prompt, then wait until it is actually pending before feeding a
// line — otherwise the line could be consumed as a command before registration.
@@ -93,6 +94,7 @@ public class ConsoleInputPumpTests
Assert.Equal("the answer", prompt.Result);
reader.Complete();
+ Assert.True(run.Wait(Timeout), "Run() did not return on EOF");
}
[Fact]
@@ -100,7 +102,7 @@ public class ConsoleInputPumpTests
{
var reader = new BlockingTextReader();
var pump = new ConsoleInputPump(reader, _ => null);
- Task.Run(pump.Run);
+ var run = Task.Run(pump.Run);
var prompt = Task.Run(pump.ReadLine);
WaitUntil(() => pump.HasPendingPrompt, "prompt did not register");
@@ -108,6 +110,7 @@ public class ConsoleInputPumpTests
Assert.True(prompt.Wait(Timeout), "prompt hung on EOF");
Assert.Null(prompt.Result);
+ Assert.True(run.Wait(Timeout), "Run() did not return on EOF");
Assert.False(pump.Running);
}
@@ -116,7 +119,7 @@ public class ConsoleInputPumpTests
{
var reader = new BlockingTextReader();
var pump = new ConsoleInputPump(reader, _ => throw new InvalidOperationException("boom"));
- Task.Run(pump.Run);
+ var run = Task.Run(pump.Run);
// Wait until the reader loop is blocked on its first read, then feed a bad command.
WaitUntil(() => reader.ReadCallCount >= 1, "reader did not start");
@@ -133,6 +136,7 @@ public class ConsoleInputPumpTests
Assert.True(prompt.Wait(Timeout), "prompt hung after throwing lookup");
Assert.Null(prompt.Result);
+ Assert.True(run.Wait(Timeout), "Run() did not return on EOF");
Assert.False(pump.Running);
}
}
diff --git a/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs b/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs
index 7fae1f3bf..cdad2512b 100644
--- a/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs
+++ b/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs
@@ -68,7 +68,7 @@ public class Point3DTests
Assert.Equal(new Point3D(101, 23, 55), Point3D.Parse(" (101,23 ,55) ", null));
// No parenthesis
- Assert.False(Point3D.TryParse("101, 23 55)", null, out p));
+ Assert.False(Point3D.TryParse("101, 23, 55)", null, out p));
Assert.Equal(default, p);
Assert.Throws(() => Point3D.Parse("101, 23, 55)", null));
diff --git a/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs b/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs
index d225b625f..9f1ffdb20 100644
--- a/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs
+++ b/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs
@@ -104,7 +104,7 @@ public class Rectangle3DTests
Assert.False(Rectangle3D.TryParse("(101,,10)+(55, 89 1)", null, out p));
Assert.Equal(default, p);
- Assert.Throws(() => Rectangle3D.Parse("(,23,)+(,89)", null));
+ Assert.Throws(() => Rectangle3D.Parse("(101,,10)+(55, 89 1)", null));
Assert.False(Rectangle3D.TryParse("(,,)+(55,89, 1)", null, out p));
Assert.Equal(default, p);
diff --git a/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs b/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs
index 561ece1f9..a913eb9a9 100644
--- a/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs
+++ b/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs
@@ -90,16 +90,16 @@ public sealed class WorldLocationTests
Assert.Equal(new WorldLocation(101, 23, 55, null), WorldLocation.Parse(" (101,23 ,55) [(-null-)]", null));
// No Brackets
- Assert.False(WorldLocation.TryParse("(101, 23 55) Felucca]", null, out p));
+ Assert.False(WorldLocation.TryParse("(101, 23, 55) Felucca]", null, out p));
Assert.Equal(default, p);
Assert.Throws(() => WorldLocation.Parse("(101, 23, 55) Felucca]", null));
- Assert.False(WorldLocation.TryParse("(101, 23 55) [Felucca", null, out p));
+ Assert.False(WorldLocation.TryParse("(101, 23, 55) [Felucca", null, out p));
Assert.Equal(default, p);
Assert.Throws(() => WorldLocation.Parse("(101, 23, 55) [Felucca", null));
// No parenthesis
- Assert.False(WorldLocation.TryParse("101, 23 55) [Felucca]", null, out p));
+ Assert.False(WorldLocation.TryParse("101, 23, 55) [Felucca]", null, out p));
Assert.Equal(default, p);
Assert.Throws(() => WorldLocation.Parse("101, 23, 55) [Felucca]", null));
diff --git a/Projects/Server.Tests/Tests/Items/ContainerBulkRemovalTests.cs b/Projects/Server.Tests/Tests/Items/ContainerBulkRemovalTests.cs
index c529f8a12..33740ec79 100644
--- a/Projects/Server.Tests/Tests/Items/ContainerBulkRemovalTests.cs
+++ b/Projects/Server.Tests/Tests/Items/ContainerBulkRemovalTests.cs
@@ -6,32 +6,43 @@ namespace Server.Tests;
[Collection("Sequential Server Tests")]
public class ContainerBulkRemovalTests
{
+ // Large enough that a quadratic child removal (a front-to-back IndexOf per child) costs
+ // roughly 2 * 10^8 comparisons and shows up in the test's run time.
+ private const int ChildCount = 20_000;
+
[Fact]
public void DeletingLargeContainer_DeletesAllChildren_AndTotalsStayConsistent()
{
var outer = new Container(0xE75);
- var pack = new Container(0xE75);
- outer.DropItem(pack);
- var children = new Item[100_000];
- for (var i = 0; i < children.Length; i++)
+ try
{
- children[i] = new Item(0x1234);
- pack.DropItem(children[i]);
+ var pack = new Container(0xE75);
+ outer.DropItem(pack);
+
+ var children = new Item[ChildCount];
+ for (var i = 0; i < children.Length; i++)
+ {
+ children[i] = new Item(0x1234);
+ pack.DropItem(children[i]);
+ }
+
+ Assert.Equal(children.Length + 1, outer.TotalItems);
+
+ pack.Delete();
+
+ for (var i = 0; i < children.Length; i++)
+ {
+ Assert.True(children[i].Deleted);
+ }
+
+ Assert.Empty(outer.Items);
+ Assert.Equal(0, outer.TotalItems);
}
-
- Assert.Equal(children.Length + 1, outer.TotalItems);
-
- pack.Delete();
-
- for (var i = 0; i < children.Length; i++)
+ finally
{
- Assert.True(children[i].Deleted);
+ outer.Delete();
}
-
- Assert.Empty(outer.Items);
- Assert.Equal(0, outer.TotalItems);
- outer.Delete();
}
// The tail fast path must not change which entry is removed when the item is elsewhere.
@@ -39,20 +50,27 @@ public class ContainerBulkRemovalTests
public void RemovingFromFrontMiddleAndEnd_RemovesExactlyThatItem()
{
var pack = new Container(0xE75);
- var items = new Item[5];
- for (var i = 0; i < items.Length; i++)
+
+ try
{
- items[i] = new Item(0x1234);
- pack.DropItem(items[i]);
+ var items = new Item[5];
+ for (var i = 0; i < items.Length; i++)
+ {
+ items[i] = new Item(0x1234);
+ pack.DropItem(items[i]);
+ }
+
+ items[0].Delete();
+ items[2].Delete();
+ items[4].Delete();
+
+ Assert.Equal(2, pack.Items.Count);
+ Assert.Same(items[1], pack.Items[0]);
+ Assert.Same(items[3], pack.Items[1]);
+ }
+ finally
+ {
+ pack.Delete();
}
-
- items[0].Delete();
- items[2].Delete();
- items[4].Delete();
-
- Assert.Equal(2, pack.Items.Count);
- Assert.Same(items[1], pack.Items[0]);
- Assert.Same(items[3], pack.Items[1]);
- pack.Delete();
}
}
diff --git a/Projects/Server.Tests/Tests/Items/ContainerChildRemovalTests.cs b/Projects/Server.Tests/Tests/Items/ContainerChildRemovalTests.cs
index 695838185..587753250 100644
--- a/Projects/Server.Tests/Tests/Items/ContainerChildRemovalTests.cs
+++ b/Projects/Server.Tests/Tests/Items/ContainerChildRemovalTests.cs
@@ -1,8 +1,7 @@
-using System;
using Server.Items;
using Server.Network;
-using Server.Tests.Network;
using Xunit;
+using static Server.Tests.TestClientHarness;
namespace Server.Tests;
@@ -44,19 +43,6 @@ public class ContainerChildRemovalTests
private static readonly Point3D _ownerLoc = new(1500, 1500, 0);
- private static (NetState, Mobile) CreateClient(Point3D location)
- {
- var ns = PacketTestUtilities.CreateTestNetState();
- ns.Account = new MockAccount();
-
- var mobile = new Mobile(World.NewMobile);
- mobile.DefaultMobileInit();
- ns.Mobile = mobile;
- mobile.NetState = ns;
- mobile.MoveToWorld(location, Map.Felucca);
- return (ns, mobile);
- }
-
private static Mobile CreateOwnerWith(Container pack)
{
var owner = new Mobile(World.NewMobile);
@@ -68,44 +54,6 @@ public class ContainerChildRemovalTests
return owner;
}
- private static bool ReceivedRemove(NetState ns, Serial serial)
- {
- Span expected = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength];
- OutgoingEntityPackets.CreateRemoveEntity(expected, serial);
- return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
- }
-
- private static int CountRemoves(NetState ns, Serial serial)
- {
- Span expected = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength];
- OutgoingEntityPackets.CreateRemoveEntity(expected, serial);
-
- var span = ns.SendBuffer.GetReadSpan();
- var count = 0;
-
- while (true)
- {
- var index = span.IndexOf(expected);
-
- if (index < 0)
- {
- break;
- }
-
- count++;
- span = span[(index + expected.Length)..];
- }
-
- return count;
- }
-
- private static void DisposeClient(NetState ns, Mobile mobile)
- {
- ns.Mobile = null;
- ns.Dispose();
- mobile.Delete();
- }
-
private static void Cleanup(NetState ns, Mobile client, Mobile owner)
{
DisposeClient(ns, client);
diff --git a/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs b/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs
index 0f97eaa28..85ffd3c84 100644
--- a/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs
+++ b/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs
@@ -5,11 +5,15 @@ using Xunit;
namespace Server.Tests;
[Collection("Sequential Server Tests")]
-public class DecayRegistrationTests
+public class DecayRegistrationTests : IDisposable
{
+ private readonly EntityCleanup _cleanup = new();
+
// The wheel is static and these tests move the clock, so start each from a known schedule.
public DecayRegistrationTests() => DecayScheduler.ResetForTests();
+ public void Dispose() => _cleanup.Dispose();
+
// Stands in for a region that refuses decay, e.g. HouseRegion over a locked down item.
private class RefusesDecayItem : Item
{
@@ -20,61 +24,34 @@ public class DecayRegistrationTests
public override bool OnDecay() => false;
}
- // The item is already dequeued, so a refusal that does not reschedule strands it forever.
- // Drives the scheduler and the game clock without depending on the shared timer wheel.
- private static void AdvanceDecay(DateTime start, TimeSpan duration, Item item)
- {
- var deadline = start + duration;
-
- for (var now = start; now <= deadline && !item.Deleted; now += TimeSpan.FromMilliseconds(256))
- {
- Core._now = now;
- DecayScheduler.ProcessTick(now);
- }
- }
-
[Fact]
public void ItemOnGround_ActuallyDecaysAfterDecayTime()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
- Assert.True(DecayScheduler.IsRegistered(item));
+ Assert.True(DecayScheduler.IsRegistered(item));
- AdvanceDecay(start, item.DecayTime + TimeSpan.FromMinutes(2), item);
+ clock.AdvanceDecay(item.DecayTime + TimeSpan.FromMinutes(2), item);
- Assert.True(item.Deleted, "Item on the ground must decay once DecayTime elapses.");
- }
- finally
- {
- Core._now = start;
- }
+ Assert.True(item.Deleted, "Item on the ground must decay once DecayTime elapses.");
}
[Fact]
public void ItemOnGround_DoesNotDecayBeforeDecayTime()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(102, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(102, 100, 0), Map.Felucca);
- AdvanceDecay(start, item.DecayTime - TimeSpan.FromMinutes(2), item);
+ clock.AdvanceDecay(item.DecayTime - TimeSpan.FromMinutes(2), item);
- Assert.False(item.Deleted, "Item must not decay before DecayTime elapses.");
+ Assert.False(item.Deleted, "Item must not decay before DecayTime elapses.");
- item.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ item.Delete();
}
// The real client path: Mobile.Lift internalizes, then Mobile.Drop calls MoveToWorld.
@@ -84,7 +61,7 @@ public class DecayRegistrationTests
[Fact]
public void DecaySlot_AgreesWithStructures_AcrossLifecycle()
{
- var item = new Item(0x1234);
+ var item = _cleanup.Track(new Item(0x1234));
item.MoveToWorld(new Point3D(103, 100, 0), Map.Felucca);
Assert.NotEqual(DecayScheduler.SlotNone, item.DecaySlot);
@@ -108,10 +85,10 @@ public class DecayRegistrationTests
[Fact]
public void ContainerDestroy_ContentsDroppedToGroundAreTracked()
{
- var pack = new Container(0xE75);
+ var pack = _cleanup.Track(new Container(0xE75));
pack.MoveToWorld(new Point3D(105, 100, 0), Map.Felucca);
- var item = new Item(0x1234);
+ var item = _cleanup.Track(new Item(0x1234));
pack.AddItem(item);
Assert.False(DecayScheduler.IsRegistered(item));
@@ -128,163 +105,127 @@ public class DecayRegistrationTests
[Fact]
public void StaleImmovableItemMadeMovable_GetsAFreshDecayWindow()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(107, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(107, 100, 0), Map.Felucca);
- item.Movable = false;
- Assert.False(DecayScheduler.IsRegistered(item), "A frozen item must not be tracked for decay.");
+ item.Movable = false;
+ Assert.False(DecayScheduler.IsRegistered(item), "A frozen item must not be tracked for decay.");
- Core._now = start + TimeSpan.FromDays(30);
- var flipped = Core._now;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
- item.Movable = true;
+ item.Movable = true;
- Assert.True(DecayScheduler.IsRegistered(item), "An unfrozen item must be tracked for decay.");
+ Assert.True(DecayScheduler.IsRegistered(item), "An unfrozen item must be tracked for decay.");
- AdvanceDecay(flipped, item.DecayTime - TimeSpan.FromMinutes(2), item);
- Assert.False(item.Deleted, "An unfrozen item must get a full decay window, not vanish immediately.");
+ clock.AdvanceDecay(item.DecayTime - TimeSpan.FromMinutes(2), item);
+ Assert.False(item.Deleted, "An unfrozen item must get a full decay window, not vanish immediately.");
- AdvanceDecay(Core._now, TimeSpan.FromMinutes(4), item);
- Assert.True(item.Deleted, "An unfrozen item must still decay once the fresh window elapses.");
- }
- finally
- {
- Core._now = start;
- }
+ clock.AdvanceDecay(TimeSpan.FromMinutes(4), item);
+ Assert.True(item.Deleted, "An unfrozen item must still decay once the fresh window elapses.");
}
// Same transition through the Visible setter: unhiding a long-hidden item.
[Fact]
public void StaleHiddenItemMadeVisible_GetsAFreshDecayWindow()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(109, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(109, 100, 0), Map.Felucca);
- item.Visible = false;
- Assert.False(DecayScheduler.IsRegistered(item), "A hidden item must not be tracked for decay.");
+ item.Visible = false;
+ Assert.False(DecayScheduler.IsRegistered(item), "A hidden item must not be tracked for decay.");
- Core._now = start + TimeSpan.FromDays(30);
- var flipped = Core._now;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
- item.Visible = true;
+ item.Visible = true;
- Assert.True(DecayScheduler.IsRegistered(item), "An unhidden item must be tracked for decay.");
+ Assert.True(DecayScheduler.IsRegistered(item), "An unhidden item must be tracked for decay.");
- AdvanceDecay(flipped, item.DecayTime - TimeSpan.FromMinutes(2), item);
- Assert.False(item.Deleted, "An unhidden item must get a full decay window, not vanish immediately.");
+ clock.AdvanceDecay(item.DecayTime - TimeSpan.FromMinutes(2), item);
+ Assert.False(item.Deleted, "An unhidden item must get a full decay window, not vanish immediately.");
- AdvanceDecay(Core._now, TimeSpan.FromMinutes(4), item);
- Assert.True(item.Deleted, "An unhidden item must still decay once the fresh window elapses.");
- }
- finally
- {
- Core._now = start;
- }
+ clock.AdvanceDecay(TimeSpan.FromMinutes(4), item);
+ Assert.True(item.Deleted, "An unhidden item must still decay once the fresh window elapses.");
}
- // A refusal restarts the countdown without rewriting LastMoved.
+ // A refusal restarts the countdown without rewriting LastMoved. The item is already dequeued
+ // when OnDecay refuses, so a refusal that does not reschedule strands it forever.
[Fact]
public void RefusedDecay_DoesNotRewriteLastMoved()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new RefusesDecayItem();
- item.MoveToWorld(new Point3D(110, 100, 0), Map.Felucca);
- var lastMoved = item.LastMoved;
+ var item = _cleanup.Track(new RefusesDecayItem());
+ item.MoveToWorld(new Point3D(110, 100, 0), Map.Felucca);
+ var lastMoved = item.LastMoved;
- AdvanceDecay(start, item.DecayTime + TimeSpan.FromMinutes(2), item);
+ clock.AdvanceDecay(item.DecayTime + TimeSpan.FromMinutes(2), item);
- Assert.False(item.Deleted, "A refused decay must not delete the item.");
- Assert.True(DecayScheduler.IsRegistered(item), "A refused decay must leave the item tracked.");
- Assert.Equal(lastMoved, item.LastMoved);
+ Assert.False(item.Deleted, "A refused decay must not delete the item.");
+ Assert.True(DecayScheduler.IsRegistered(item), "A refused decay must leave the item tracked.");
+ Assert.Equal(lastMoved, item.LastMoved);
- item.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ item.Delete();
}
// The fresh window must survive a save/load cycle, or a restart mid-window deletes the item.
[Fact]
public void FreshDecayWindow_SurvivesSerialization()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(111, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(111, 100, 0), Map.Felucca);
- item.Movable = false;
- Core._now = start + TimeSpan.FromDays(30);
- item.Movable = true;
+ item.Movable = false;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
+ item.Movable = true;
- var expected = item.ScheduledDecayTime;
+ var expected = item.ScheduledDecayTime;
- var writer = new BufferWriter(new byte[512], true);
- item.Serialize(writer);
+ var writer = new BufferWriter(new byte[512], true);
+ item.Serialize(writer);
- var copy = new Item(item.Serial);
- copy.Deserialize(new BufferReader(writer.Buffer));
+ var copy = new Item(item.Serial);
+ copy.Deserialize(new BufferReader(writer.Buffer));
- // The stamp is stored as a delta, so it ages only by the real time between
- // write and read - milliseconds here, the downtime in production.
- Assert.True(
- (copy.ScheduledDecayTime - expected).Duration() <= TimeSpan.FromSeconds(5),
- "The restarted decay window must survive a save/load cycle."
- );
+ // The stamp is stored as a delta, so it ages only by the real time between
+ // write and read - milliseconds here, the downtime in production.
+ Assert.True(
+ (copy.ScheduledDecayTime - expected).Duration() <= TimeSpan.FromSeconds(5),
+ "The restarted decay window must survive a save/load cycle."
+ );
- item.Delete();
- copy.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ item.Delete();
+ copy.Delete();
}
// A real move supersedes the reset stamp; it must be dropped so the CompactInfo can collapse.
[Fact]
public void MovingAnItem_ClearsASupersededDecayResetStamp()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(112, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(112, 100, 0), Map.Felucca);
- item.Movable = false;
- Core._now = start + TimeSpan.FromDays(30);
- item.Movable = true;
+ item.Movable = false;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
+ item.Movable = true;
- Assert.NotEqual(default, item.DecayResetTime);
+ Assert.NotEqual(default, item.DecayResetTime);
- Core._now += TimeSpan.FromMinutes(1);
- item.MoveToWorld(new Point3D(113, 100, 0), Map.Felucca);
+ clock.Now += TimeSpan.FromMinutes(1);
+ item.MoveToWorld(new Point3D(113, 100, 0), Map.Felucca);
- Assert.Equal(default, item.DecayResetTime);
- Assert.Equal(item.LastMoved + item.DecayTime, item.ScheduledDecayTime);
- Assert.True(DecayScheduler.IsRegistered(item));
+ Assert.Equal(default, item.DecayResetTime);
+ Assert.Equal(item.LastMoved + item.DecayTime, item.ScheduledDecayTime);
+ Assert.True(DecayScheduler.IsRegistered(item));
- item.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ item.Delete();
}
// Losing decay eligibility makes the stamp meaningless; it must be dropped so the
@@ -292,29 +233,22 @@ public class DecayRegistrationTests
[Fact]
public void ItemBecomingIneligible_DropsTheDecayResetStamp()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(115, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(115, 100, 0), Map.Felucca);
- item.Movable = false;
- Core._now = start + TimeSpan.FromDays(30);
- item.Movable = true;
+ item.Movable = false;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
+ item.Movable = true;
- Assert.NotEqual(default, item.DecayResetTime);
+ Assert.NotEqual(default, item.DecayResetTime);
- item.Movable = false;
+ item.Movable = false;
- Assert.Equal(default, item.DecayResetTime);
+ Assert.Equal(default, item.DecayResetTime);
- item.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ item.Delete();
}
// Moving a stamped item into a container programmatically (no drop, no SetLastMoved)
@@ -322,32 +256,25 @@ public class DecayRegistrationTests
[Fact]
public void StampedItemAddedToContainer_DropsTheDecayResetStamp()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var pack = new Container(0xE75);
- pack.MoveToWorld(new Point3D(116, 100, 0), Map.Felucca);
+ var pack = _cleanup.Track(new Container(0xE75));
+ pack.MoveToWorld(new Point3D(116, 100, 0), Map.Felucca);
- var item = new Item(0x1234);
- item.MoveToWorld(new Point3D(117, 100, 0), Map.Felucca);
+ var item = _cleanup.Track(new Item(0x1234));
+ item.MoveToWorld(new Point3D(117, 100, 0), Map.Felucca);
- item.Movable = false;
- Core._now = start + TimeSpan.FromDays(30);
- item.Movable = true;
+ item.Movable = false;
+ clock.Now = clock.Start + TimeSpan.FromDays(30);
+ item.Movable = true;
- Assert.NotEqual(default, item.DecayResetTime);
+ Assert.NotEqual(default, item.DecayResetTime);
- pack.AddItem(item);
+ pack.AddItem(item);
- Assert.Equal(default, item.DecayResetTime);
+ Assert.Equal(default, item.DecayResetTime);
- pack.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ pack.Delete();
}
// A raw Map assignment (e.g. a GM changing Map through props) is a move: it must
@@ -355,7 +282,7 @@ public class DecayRegistrationTests
[Fact]
public void ItemMovedToRealMapViaMapSetter_IsRegisteredForDecay()
{
- var item = new Item(0x1234);
+ var item = _cleanup.Track(new Item(0x1234));
Assert.False(DecayScheduler.IsRegistered(item));
item.Map = Map.Felucca;
@@ -371,10 +298,10 @@ public class DecayRegistrationTests
[Fact]
public void PartialLiftOfGroundStack_LeavesRemainderRegisteredForDecay()
{
- var stack = new Item(0x1234) { Stackable = true, Amount = 10 };
+ var stack = _cleanup.Track(new Item(0x1234) { Stackable = true, Amount = 10 });
stack.MoveToWorld(new Point3D(114, 100, 0), Map.Felucca);
- var remainder = Mobile.LiftItemDupe(stack, 3);
+ var remainder = _cleanup.Track(Mobile.LiftItemDupe(stack, 3));
Assert.NotNull(remainder);
Assert.Equal(7, remainder.Amount);
@@ -388,6 +315,4 @@ public class DecayRegistrationTests
stack.Delete();
remainder.Delete();
}
-
- // Dropping into a container must untrack; taking it back out to the ground must re-track.
}
diff --git a/Projects/Server.Tests/Tests/Items/LiftRejectResyncTests.cs b/Projects/Server.Tests/Tests/Items/LiftRejectResyncTests.cs
index 97cffbba9..03b372e14 100644
--- a/Projects/Server.Tests/Tests/Items/LiftRejectResyncTests.cs
+++ b/Projects/Server.Tests/Tests/Items/LiftRejectResyncTests.cs
@@ -1,9 +1,7 @@
-using System;
-using System.Buffers;
using Server.Items;
using Server.Network;
-using Server.Tests.Network;
using Xunit;
+using static Server.Tests.TestClientHarness;
namespace Server.Tests;
@@ -12,51 +10,6 @@ public class LiftRejectResyncTests
{
private static readonly Point3D _baseLoc = new(2200, 2200, 0);
- private static (NetState, Mobile) CreateClient(Point3D location)
- {
- var ns = PacketTestUtilities.CreateTestNetState();
- ns.Account = new MockAccount();
-
- var mobile = new Mobile(World.NewMobile);
- mobile.DefaultMobileInit();
- ns.Mobile = mobile;
- mobile.NetState = ns;
- mobile.MoveToWorld(location, Map.Felucca);
- return (ns, mobile);
- }
-
- private static void DisposeClient(NetState ns, Mobile mobile)
- {
- ns.Mobile = null;
- ns.Dispose();
- mobile.Delete();
- }
-
- private static bool ReceivedContentUpdate(NetState ns, Serial serial)
- {
- Span expected = stackalloc byte[5];
- var writer = new SpanWriter(expected);
- writer.Write((byte)0x25); // ContainerContentUpdate packet ID
- writer.Write(serial);
- return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
- }
-
- private static bool ReceivedWorldItem(NetState ns, Item item)
- {
- Span expected = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength];
- var length = OutgoingItemPackets.CreateWorldItem(expected, item);
- return ns.SendBuffer.GetReadSpan().IndexOf(expected[..length]) >= 0;
- }
-
- private static bool ReceivedEquipUpdate(NetState ns, Serial serial)
- {
- Span expected = stackalloc byte[5];
- var writer = new SpanWriter(expected);
- writer.Write((byte)0x2E); // EquipUpdate packet ID
- writer.Write(serial);
- return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
- }
-
[Fact]
public void PrivateNestedItem_RejectedLift_DoesNotResyncBystander()
{
diff --git a/Projects/Server.Tests/Tests/Items/LockedDownContainerDecayTests.cs b/Projects/Server.Tests/Tests/Items/LockedDownContainerDecayTests.cs
index b8b4fd419..e7d790b7b 100644
--- a/Projects/Server.Tests/Tests/Items/LockedDownContainerDecayTests.cs
+++ b/Projects/Server.Tests/Tests/Items/LockedDownContainerDecayTests.cs
@@ -5,8 +5,10 @@ using Xunit;
namespace Server.Tests;
[Collection("Sequential Server Tests")]
-public class LockedDownContainerDecayTests
+public class LockedDownContainerDecayTests : IDisposable
{
+ private readonly EntityCleanup _cleanup = new();
+
public LockedDownContainerDecayTests()
{
DecayScheduler.ResetForTests();
@@ -19,6 +21,8 @@ public class LockedDownContainerDecayTests
}
}
+ public void Dispose() => _cleanup.Dispose();
+
private class TestChest : Container
{
public TestChest() : base(0xE43)
@@ -35,11 +39,11 @@ public class LockedDownContainerDecayTests
public override bool ContentsDecay => false;
}
- private static Item NewLoot() => new Item(0x1F03) { Movable = true, Visible = true };
+ private Item NewLoot() => _cleanup.Track(new Item(0x1F03) { Movable = true, Visible = true });
- private static TestChest PlaceLockedDownChest(int x)
+ private TestChest PlaceLockedDownChest(int x)
{
- var chest = new TestChest { Movable = false };
+ var chest = _cleanup.Track(new TestChest { Movable = false });
chest.MoveToWorld(new Point3D(x, 100, 0), Map.Felucca);
chest.IsLockedDown = true;
return chest;
@@ -62,7 +66,7 @@ public class LockedDownContainerDecayTests
[Fact]
public void ItemInOrdinaryContainer_IsNotTracked()
{
- var chest = new TestChest();
+ var chest = _cleanup.Track(new TestChest());
chest.MoveToWorld(new Point3D(121, 100, 0), Map.Felucca);
var loot = NewLoot();
@@ -77,7 +81,7 @@ public class LockedDownContainerDecayTests
[Fact]
public void LockingAndReleasingContainer_RegistersAndUnregistersContents()
{
- var chest = new TestChest { Movable = false };
+ var chest = _cleanup.Track(new TestChest { Movable = false });
chest.MoveToWorld(new Point3D(122, 100, 0), Map.Felucca);
var loot = NewLoot();
chest.AddItem(loot);
@@ -102,7 +106,7 @@ public class LockedDownContainerDecayTests
[Fact]
public void ContainerThatKeepsContents_DoesNotTrackThem()
{
- var board = new KeepsContentsChest { Movable = false };
+ var board = _cleanup.Track(new KeepsContentsChest { Movable = false });
board.MoveToWorld(new Point3D(123, 100, 0), Map.Felucca);
board.IsLockedDown = true;
var piece = NewLoot();
@@ -118,7 +122,7 @@ public class LockedDownContainerDecayTests
public void NestedContainerContents_AreNotTracked()
{
var chest = PlaceLockedDownChest(124);
- var bag = new TestChest();
+ var bag = _cleanup.Track(new TestChest());
chest.AddItem(bag);
var loot = NewLoot();
bag.AddItem(loot);
@@ -145,29 +149,17 @@ public class LockedDownContainerDecayTests
[Fact]
public void TrackedContents_DecayOnSchedule()
{
- var start = Core._now;
+ using var clock = new SimulatedClockScope();
- try
- {
- var chest = PlaceLockedDownChest(126);
- var loot = NewLoot();
- chest.AddItem(loot);
+ var chest = PlaceLockedDownChest(126);
+ var loot = NewLoot();
+ chest.AddItem(loot);
- var deadline = start + loot.DecayTime + TimeSpan.FromMinutes(2);
- for (var now = start; now <= deadline && !loot.Deleted; now += TimeSpan.FromMilliseconds(256))
- {
- Core._now = now;
- DecayScheduler.ProcessTick(now);
- }
+ clock.AdvanceDecay(loot.DecayTime + TimeSpan.FromMinutes(2), loot);
- Assert.True(loot.Deleted, "Contents of a locked-down container must decay.");
- Assert.False(chest.Deleted, "The locked-down container itself must not decay.");
+ Assert.True(loot.Deleted, "Contents of a locked-down container must decay.");
+ Assert.False(chest.Deleted, "The locked-down container itself must not decay.");
- chest.Delete();
- }
- finally
- {
- Core._now = start;
- }
+ chest.Delete();
}
}
diff --git a/Projects/Server.Tests/Tests/Items/PrivateChildPropertiesTests.cs b/Projects/Server.Tests/Tests/Items/PrivateChildPropertiesTests.cs
index 4481b08b2..97b67613f 100644
--- a/Projects/Server.Tests/Tests/Items/PrivateChildPropertiesTests.cs
+++ b/Projects/Server.Tests/Tests/Items/PrivateChildPropertiesTests.cs
@@ -1,15 +1,24 @@
using System;
-using System.Buffers;
using Server.Items;
using Server.Network;
-using Server.Tests.Network;
using Xunit;
+using static Server.Tests.TestClientHarness;
namespace Server.Tests;
[Collection("Sequential Server Tests")]
-public class PrivateChildPropertiesTests
+public class PrivateChildPropertiesTests : IDisposable
{
+ private readonly bool _oplWasEnabled;
+
+ public PrivateChildPropertiesTests()
+ {
+ _oplWasEnabled = ObjectPropertyList.Enabled;
+ ObjectPropertyList.Enabled = true;
+ }
+
+ public void Dispose() => ObjectPropertyList.Enabled = _oplWasEnabled;
+
private class PublicContainer : Container
{
public PublicContainer() : base(0xE75)
@@ -21,35 +30,6 @@ public class PrivateChildPropertiesTests
private static readonly Point3D _ownerLoc = new(1600, 1600, 0);
- private static (NetState, Mobile) CreateClient(Point3D location)
- {
- var ns = PacketTestUtilities.CreateTestNetState();
- ns.Account = new MockAccount();
-
- var mobile = new Mobile(World.NewMobile);
- mobile.DefaultMobileInit();
- ns.Mobile = mobile;
- mobile.NetState = ns;
- mobile.MoveToWorld(location, Map.Felucca);
- return (ns, mobile);
- }
-
- private static void DisposeClient(NetState ns, Mobile mobile)
- {
- ns.Mobile = null;
- ns.Dispose();
- mobile.Delete();
- }
-
- private static bool ReceivedOplInfo(NetState ns, Serial serial)
- {
- Span expected = stackalloc byte[5];
- var writer = new SpanWriter(expected);
- writer.Write((byte)0xDC); // OPL info packet ID
- writer.Write(serial);
- return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
- }
-
// AddItem/MoveToWorld already queue their own Update (+Properties) delta as a side effect of the
// Map setter. Draining it first isolates the properties-only delta the test actually exercises;
// otherwise the leftover Update flag routes through the Update branch and the test would pass or
@@ -68,9 +48,6 @@ public class PrivateChildPropertiesTests
[Fact]
public void PrivateChildInOwnerBackpack_PropertiesOnlyUpdate_ReachesOwnerNotBystander()
{
- var wasEnabled = ObjectPropertyList.Enabled;
- ObjectPropertyList.Enabled = true;
-
var (ownerNs, owner) = CreateClient(_ownerLoc);
var (bystanderNs, bystander) = CreateClient(new Point3D(_ownerLoc.X + 1, _ownerLoc.Y, 0));
@@ -93,16 +70,12 @@ public class PrivateChildPropertiesTests
child.Delete();
DisposeClient(ownerNs, owner);
DisposeClient(bystanderNs, bystander);
- ObjectPropertyList.Enabled = wasEnabled;
}
}
[Fact]
public void PrivateGroundContainer_PropertiesOnlyUpdate_ReachesOpenerNotNonOpener()
{
- var wasEnabled = ObjectPropertyList.Enabled;
- ObjectPropertyList.Enabled = true;
-
var (openerNs, opener) = CreateClient(_ownerLoc);
var (bystanderNs, bystander) = CreateClient(new Point3D(_ownerLoc.X + 2, _ownerLoc.Y, 0));
@@ -126,16 +99,12 @@ public class PrivateChildPropertiesTests
container.Delete();
DisposeClient(openerNs, opener);
DisposeClient(bystanderNs, bystander);
- ObjectPropertyList.Enabled = wasEnabled;
}
}
[Fact]
public void PublicContainerChild_PropertiesOnlyUpdate_StillReachesBystander()
{
- var wasEnabled = ObjectPropertyList.Enabled;
- ObjectPropertyList.Enabled = true;
-
var (bystanderNs, bystander) = CreateClient(_ownerLoc);
var pack = new PublicContainer();
@@ -155,16 +124,12 @@ public class PrivateChildPropertiesTests
{
pack.Delete();
DisposeClient(bystanderNs, bystander);
- ObjectPropertyList.Enabled = wasEnabled;
}
}
[Fact]
public void GroundItem_PropertiesOnlyUpdate_StillReachesBystander()
{
- var wasEnabled = ObjectPropertyList.Enabled;
- ObjectPropertyList.Enabled = true;
-
var (bystanderNs, bystander) = CreateClient(_ownerLoc);
var item = new Item(0x1234);
@@ -181,7 +146,6 @@ public class PrivateChildPropertiesTests
{
item.Delete();
DisposeClient(bystanderNs, bystander);
- ObjectPropertyList.Enabled = wasEnabled;
}
}
}
diff --git a/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs b/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs
index a240a0576..c4c48b41b 100644
--- a/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs
+++ b/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs
@@ -1,18 +1,28 @@
+using System;
using Xunit;
namespace Server.Tests;
[Collection("Sequential Server Tests")]
-public class LocalizationEntryTests
+public class LocalizationEntryTests : IDisposable
{
+ private const int FormatNumber = 500002;
+ private const int ParameterNumber = 500003;
+
+ public void Dispose()
+ {
+ Localization.Remove("enu", FormatNumber);
+ Localization.Remove("enu", ParameterNumber);
+ }
+
[Fact]
public void TestClilocAsParameter()
{
- Localization.Add("enu", 500002, "This tests ~1_NUMBER~ as parameters.");
- Localization.Add("enu", 500003, "clilocs");
+ Localization.Add("enu", FormatNumber, "This tests ~1_NUMBER~ as parameters.");
+ Localization.Add("enu", ParameterNumber, "clilocs");
- string numericFormatter = Localization.Format(500002, "enu", $"{500003:#}");
- string stringParam = Localization.Format(500002, "enu", $"{"#500003"}");
+ string numericFormatter = Localization.Format(FormatNumber, "enu", $"{ParameterNumber:#}");
+ string stringParam = Localization.Format(FormatNumber, "enu", $"{"#500003"}");
Assert.Equal("This tests clilocs as parameters.", numericFormatter);
Assert.Equal("This tests clilocs as parameters.", stringParam);
diff --git a/Projects/Server.Tests/Tests/Maps/CanFitItemTests.cs b/Projects/Server.Tests/Tests/Maps/CanFitItemTests.cs
index 684c77743..5531e0530 100644
--- a/Projects/Server.Tests/Tests/Maps/CanFitItemTests.cs
+++ b/Projects/Server.Tests/Tests/Maps/CanFitItemTests.cs
@@ -33,6 +33,8 @@ public class CanFitItemTests
Assert.False(map.CanFitItem(100, map.Height + 1, 0, 1));
}
+ // Plain walkable land in the real Felucca map. Without map files every cell is the default land
+ // tile at Z 0 with no flags, which these tests treat the same way: a surface with nothing above it.
private const int TestLandX = 1500;
private const int TestLandY = 1600;
@@ -170,23 +172,19 @@ public class CanFitItemTests
Item movableItem = null;
try
{
- // Create a movable surface item
+ // Raised well clear of the land so the movable item's top is the only candidate surface there
+ var itemZ = map.GetAverageZ(x, y) + 20;
+
movableItem = new Item(ServerFixture.SurfaceTileId)
{
Movable = true
};
- movableItem.MoveToWorld(new Point3D(x, y, 10), map);
+ movableItem.MoveToWorld(new Point3D(x, y, itemZ), map);
- var itemTop = 10 + movableItem.ItemData.CalcHeight;
- // Try placing at item top - should fail unless there's land there too
- var avgZ = map.GetAverageZ(x, y);
+ var itemTop = itemZ + movableItem.ItemData.CalcHeight;
+ var result = map.CanFitItem(x, y, itemTop, 1);
- // If avgZ != itemTop, there's no other surface, so it should fail
- if (avgZ != itemTop)
- {
- var result = map.CanFitItem(x, y, itemTop, 1);
- Assert.False(result, "Movable item should not count as valid surface");
- }
+ Assert.False(result, "Movable item should not count as valid surface");
}
finally
{
@@ -248,16 +246,4 @@ public class CanFitItemTests
multi.MoveToWorld(location, map);
return multi;
}
-
- private class TestMulti : BaseMulti
- {
- private readonly MultiComponentList _components;
-
- public TestMulti(MultiComponentList components) : base(0x1)
- {
- _components = components ?? MultiComponentList.Empty;
- }
-
- public override MultiComponentList Components => _components;
- }
}
diff --git a/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTests.cs b/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTests.cs
index c90cce9ea..23bc9d173 100644
--- a/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTests.cs
+++ b/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTests.cs
@@ -29,7 +29,8 @@ public class CanSpawnMobileTests
Assert.False(map.CanSpawnMobile(100, map.Height + 1, -128, 127, false, false, out _));
}
- // Use coordinates near Britain which should be walkable land in both test and real data
+ // Plain walkable land near Britain in the real Felucca map. Without map files every cell is the
+ // default land tile at Z 0 with no flags, which CanSpawnMobile treats the same way: walkable, not water.
private const int TestLandX = 1500;
private const int TestLandY = 1600;
@@ -37,10 +38,9 @@ public class CanSpawnMobileTests
public void CanSpawnMobile_EmptyLocationWithLandReturnsTrue()
{
var map = Map.Felucca;
- map.GetAverageZ(TestLandX, TestLandY, out _, out var landZ, out _);
// Test with full Z range to find land
- var result = map.CanSpawnMobile(TestLandX, TestLandY, -128, 127, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(TestLandX, TestLandY, -128, 127, false, false, out _);
// Should find a valid spawn point (land surface)
Assert.True(result, $"Expected to find valid spawn point on land at ({TestLandX}, {TestLandY})");
@@ -53,7 +53,7 @@ public class CanSpawnMobileTests
map.GetAverageZ(TestLandX, TestLandY, out _, out var avgZ, out _);
// Use a Z range that excludes the land surface (50+ units above land)
- var result = map.CanSpawnMobile(TestLandX, TestLandY, avgZ + 50, avgZ + 100, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(TestLandX, TestLandY, avgZ + 50, avgZ + 100, false, false, out _);
Assert.False(result, $"Expected no spawn point above land at Z={avgZ}");
}
@@ -63,7 +63,7 @@ public class CanSpawnMobileTests
{
var map = Map.Felucca;
// cantWalk=true means the mob can only swim, so land shouldn't be valid
- var result = map.CanSpawnMobile(TestLandX, TestLandY, -128, 127, false, true, out var spawnZ);
+ var result = map.CanSpawnMobile(TestLandX, TestLandY, -128, 127, false, true, out _);
Assert.False(result, "cantWalk=true should not find land as valid surface");
}
@@ -83,7 +83,7 @@ public class CanSpawnMobileTests
blockingMobile.MoveToWorld(new Point3D(TestLandX, TestLandY, landZ), map);
// The mobile should block spawning at its location and Z
- var result = map.CanSpawnMobile(TestLandX, TestLandY, landZ - 16, landZ + 16, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(TestLandX, TestLandY, landZ - 16, landZ + 16, false, false, out _);
// Land surface is blocked by mobile at the same Z
Assert.False(result, "Mobile should block spawn at same location");
@@ -115,7 +115,7 @@ public class CanSpawnMobileTests
gm.MoveToWorld(new Point3D(x, y, landZ), map);
// Hidden GM should not block spawning
- var result = map.CanSpawnMobile(x, y, landZ - 16, landZ + 16, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, landZ - 16, landZ + 16, false, false, out _);
Assert.True(result, "Hidden GM should not block spawn");
}
@@ -142,7 +142,7 @@ public class CanSpawnMobileTests
blockingMobile.MoveToWorld(new Point3D(x, y, landZ + 50), map);
// Land should still be valid (mobile above doesn't block ground level)
- var result = map.CanSpawnMobile(x, y, landZ - 16, landZ + 16, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, landZ - 16, landZ + 16, false, false, out _);
Assert.True(result, "Mobile at different Z should not block");
}
diff --git a/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTileDataTests.cs b/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTileDataTests.cs
index ea12926f1..38d757463 100644
--- a/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTileDataTests.cs
+++ b/Projects/Server.Tests/Tests/Maps/CanSpawnMobileTileDataTests.cs
@@ -11,13 +11,6 @@ namespace Server.Tests.Maps;
[Collection("Sequential Server Tests")]
public class CanSpawnMobileTileDataTests
{
- private readonly ServerFixture _fixture;
-
- public CanSpawnMobileTileDataTests(ServerFixture fixture)
- {
- _fixture = fixture;
- }
-
private void SkipIfNoTileData()
{
Skip.If(!ServerFixture.TileDataLoaded, "TileData not loaded - client files required");
@@ -96,7 +89,7 @@ public class CanSpawnMobileTileDataTests
multi = CreateFloorWithCeilingMulti(map, new Point3D(x, y, 0), floorZ: 20, ceilingZ: 28);
// The low ceiling should block spawning (need 16 units clearance for mobiles)
- var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out _);
Assert.False(result);
}
@@ -121,7 +114,7 @@ public class CanSpawnMobileTileDataTests
// Create a multi with floor at Z=20 and ceiling at Z=50 (30 units clearance)
multi = CreateFloorWithCeilingMulti(map, new Point3D(x, y, 0), floorZ: 20, ceilingZ: 50);
- var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out _);
Assert.True(result);
}
@@ -147,7 +140,7 @@ public class CanSpawnMobileTileDataTests
multi = CreateFloorWithCeilingMulti(map, new Point3D(x, y, 0), floorZ: 20, ceilingZ: 100);
// The blocker at Z=100 shouldn't affect spawning in range 15-35
- var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, 15, 35, false, false, out _);
Assert.True(result);
}
@@ -173,7 +166,7 @@ public class CanSpawnMobileTileDataTests
multi = CreateWaterMulti(map, new Point3D(x, y, 0), waterZ: 0);
// canSwim=true should find the water surface
- var result = map.CanSpawnMobile(x, y, -10, 10, true, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, -10, 10, true, false, out _);
Assert.True(result);
}
@@ -202,7 +195,7 @@ public class CanSpawnMobileTileDataTests
};
surfaceItem.MoveToWorld(new Point3D(x, y, 10), map);
- var result = map.CanSpawnMobile(x, y, 5, 30, false, false, out var spawnZ);
+ var result = map.CanSpawnMobile(x, y, 5, 30, false, false, out _);
// Should find either land at Z=0 or the item surface, whichever is lowest
Assert.True(result);
@@ -274,16 +267,4 @@ public class CanSpawnMobileTileDataTests
new MultiTileEntry(ServerFixture.WetTileId, 0, 0, (short)waterZ, TileFlag.Wet)
]);
}
-
- private class TestMulti : BaseMulti
- {
- private readonly MultiComponentList _components;
-
- public TestMulti(MultiComponentList components) : base(0x1)
- {
- _components = components ?? MultiComponentList.Empty;
- }
-
- public override MultiComponentList Components => _components;
- }
}
diff --git a/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs
index c5ed6c517..718fadefc 100644
--- a/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs
+++ b/Projects/Server.Tests/Tests/Maps/MultiEnumeratorTests.cs
@@ -313,7 +313,7 @@ public class MultiEnumeratorTests
var map = Map.Felucca;
// Components reach two tiles either side, so at x = 1024 the multi spans sectors 63 and 64
- var spanning = new SpanningMulti();
+ var spanning = new TestMulti(SpanningComponents);
try
{
spanning.MoveToWorld(new Point3D(1024, 1000, 0), map);
@@ -351,29 +351,11 @@ public class MultiEnumeratorTests
}
}
- // Test implementation of BaseMulti
- private class TestMulti : BaseMulti
- {
- public TestMulti() : base(0x1)
- {
- }
- }
-
- private class SpanningMulti : BaseMulti
- {
- // Entries after the first need non-zero flags or MultiComponentList drops them
- private static readonly MultiComponentList _components = new(
- [
- new MultiTileEntry(0x1, -2, 0, 0, TileFlag.Background),
- new MultiTileEntry(0x1, 2, 0, 0, TileFlag.Background)
- ]
- );
-
- public SpanningMulti() : base(0x1)
- {
- }
-
- public override MultiComponentList Components => _components;
- }
+ // Entries after the first need non-zero flags or MultiComponentList drops them
+ private static readonly MultiComponentList SpanningComponents = new(
+ [
+ new MultiTileEntry(0x1, -2, 0, 0, TileFlag.Background),
+ new MultiTileEntry(0x1, 2, 0, 0, TileFlag.Background)
+ ]
+ );
}
-
diff --git a/Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs b/Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs
index 07b8e37d3..9c6e01a36 100644
--- a/Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs
+++ b/Projects/Server.Tests/Tests/Maps/StaticTileEnumeratorTests.cs
@@ -13,21 +13,17 @@ public class StaticTileEnumeratorTests
var map = Map.Felucca;
var location = new Point2D(200, 200);
+ // Real map files may have statics here; without them there are none. Either way the multi adds nothing.
+ var mapStatics = CollectTiles(map, location, includeStatics: true, includeMultis: false);
+
TestMulti multi = null;
try
{
- // Create a multi at the location
multi = CreateMultiWithComponents(map, new Point3D(200, 200, 0));
- // Get tiles with statics only (no multis)
- var tiles = new List();
- foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: true, includeMultis: false))
- {
- tiles.Add(tile);
- }
+ var tiles = CollectTiles(map, location, includeStatics: true, includeMultis: false);
- // Should not include multi tiles
- Assert.Empty(tiles);
+ Assert.Equal(mapStatics, tiles);
}
finally
{
@@ -69,21 +65,20 @@ public class StaticTileEnumeratorTests
var map = Map.Felucca;
var location = new Point2D(400, 400);
+ var mapStatics = CollectTiles(map, location, includeStatics: true, includeMultis: false);
+
TestMulti multi = null;
try
{
- // Create a multi at the location
multi = CreateMultiWithComponents(map, new Point3D(400, 400, 0));
- // Get all tiles (statics and multis)
- var tiles = new List();
- foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics: true, includeMultis: true))
- {
- tiles.Add(tile);
- }
+ var multiTiles = CollectTiles(map, location, includeStatics: false, includeMultis: true);
+ var tiles = CollectTiles(map, location, includeStatics: true, includeMultis: true);
- // Should include multi tiles (statics would be empty without map files)
- Assert.NotEmpty(tiles);
+ Assert.NotEmpty(multiTiles);
+ Assert.Equal(mapStatics.Count + multiTiles.Count, tiles.Count);
+ Assert.All(mapStatics, tile => Assert.Contains(tile, tiles));
+ Assert.All(multiTiles, tile => Assert.Contains(tile, tiles));
}
finally
{
@@ -257,10 +252,10 @@ public class StaticTileEnumeratorTests
{
// Outer's bounds cover the probe cell but it has no tile there; placing it first puts it
// ahead of inner in the sector's multi list
- outer = new TestMulti(World.NewItem, GappedComponents);
+ outer = new TestMulti(GappedComponents);
outer.MoveToWorld(new Point3D(1100, 1100, 0), map);
- inner = new TestMulti(World.NewItem, SingleTileComponents);
+ inner = new TestMulti(SingleTileComponents);
inner.MoveToWorld(new Point3D(location.X, location.Y, 20), map);
var tiles = new List();
@@ -294,28 +289,28 @@ public class StaticTileEnumeratorTests
]
);
+ private static readonly MultiComponentList DefaultComponents = new(
+ [
+ new MultiTileEntry(0x1, 0, 0, 0, 0x0),
+ new MultiTileEntry(0x2, 1, 0, 0, 0x0)
+ ]
+ );
+
+ private static List CollectTiles(Map map, Point2D location, bool includeStatics, bool includeMultis)
+ {
+ var tiles = new List();
+ foreach (var tile in new Map.StaticTileEnumerable(map, location, includeStatics, includeMultis))
+ {
+ tiles.Add(tile);
+ }
+
+ return tiles;
+ }
+
private static TestMulti CreateMultiWithComponents(Map map, Point3D location)
{
- var multi = new TestMulti(World.NewItem);
+ var multi = new TestMulti(DefaultComponents);
multi.MoveToWorld(location, map);
return multi;
}
-
- private class TestMulti : BaseMulti
- {
- private readonly MultiComponentList _components;
-
- public TestMulti(Serial serial, MultiComponentList components = null) : base(serial) =>
- _components = components ?? DefaultComponents;
-
- public override MultiComponentList Components => _components;
-
- private static readonly MultiComponentList DefaultComponents = new(
- [
- new MultiTileEntry(0x1, 0, 0, 0, 0x0),
- new MultiTileEntry(0x2, 1, 0, 0, 0x0)
- ]
- );
- }
}
-
diff --git a/Projects/Server.Tests/Tests/Network/Bans/BanChannelTests.cs b/Projects/Server.Tests/Tests/Network/Bans/BanChannelTests.cs
index 5eb1895d8..931947a55 100644
--- a/Projects/Server.Tests/Tests/Network/Bans/BanChannelTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Bans/BanChannelTests.cs
@@ -7,8 +7,18 @@ using Xunit;
namespace Server.Tests.Network.Bans;
-public class BanChannelTests
+[Collection("Sequential Server Tests")]
+public class BanChannelTests : IDisposable
{
+ private readonly IBanReporter[] _previousReporters = [..BanChannel.Reporters];
+ private readonly Func _previousIsExempt = BanChannel.IsExempt;
+
+ public void Dispose()
+ {
+ BanChannel.ConfigureForTesting(_previousReporters);
+ BanChannel.IsExempt = _previousIsExempt;
+ }
+
private sealed class FakeReporter : IBanReporter
{
public readonly List<(IPAddress ip, TimeSpan ttl, string reason)> Reports = [];
@@ -69,18 +79,11 @@ public class BanChannelTests
var exempt = IPAddress.Parse("203.0.113.7");
BanChannel.IsExempt = (ip, _) => ip.Equals(exempt);
- try
- {
- BanChannel.Report(exempt, TimeSpan.FromHours(1), "rate-limit");
- Assert.Empty(reporter.Reports); // escalation suppressed; the local gate already acted
+ BanChannel.Report(exempt, TimeSpan.FromHours(1), "rate-limit");
+ Assert.Empty(reporter.Reports); // escalation suppressed; the local gate already acted
- BanChannel.Report(IPAddress.Parse("203.0.113.8"), TimeSpan.FromHours(1), "rate-limit");
- Assert.Single(reporter.Reports); // everyone else is still contributed
- }
- finally
- {
- BanChannel.IsExempt = null;
- }
+ BanChannel.Report(IPAddress.Parse("203.0.113.8"), TimeSpan.FromHours(1), "rate-limit");
+ Assert.Single(reporter.Reports); // everyone else is still contributed
}
[Fact]
diff --git a/Projects/Server.Tests/Tests/Network/MovementThrottleLatencyTests.cs b/Projects/Server.Tests/Tests/Network/MovementThrottleLatencyTests.cs
index 8644993ab..4e9c82ff5 100644
--- a/Projects/Server.Tests/Tests/Network/MovementThrottleLatencyTests.cs
+++ b/Projects/Server.Tests/Tests/Network/MovementThrottleLatencyTests.cs
@@ -4,89 +4,20 @@ using Xunit;
namespace Server.Tests.Network;
///
-/// Manual tests for MovementThrottle latency scenarios.
-/// These tests simulate specific timing scenarios and should NOT run in CI/CD.
-///
-/// To run these tests manually:
-/// dotnet test --filter "Category=Manual"
-///
-/// To exclude from CI/CD, add to your test command:
-/// dotnet test --filter "Category!=Manual"
+/// MovementThrottle burst forgiveness against a full, wrapped movement history.
///
[Collection("Sequential Server Tests")]
-[Trait("Category", "Manual")]
public class MovementThrottleLatencyTests
{
- ///
- /// Sets the simulated tick count for testing.
- ///
- private static void SetTickCount(long ticks)
- {
- Core._tickCount = ticks;
- }
-
- ///
- /// Simulates recording movements at specific intervals.
- ///
- private static void SimulateMovements(NetState ns, params (int delayMs, int costMs)[] movements)
- {
- ns._movementHistory ??= new NetState.MovementRecord[20];
- ns._movementHistoryIndex = 0;
- ns._movementHistoryFull = false;
- ns._lastMovementRecordTime = 0;
-
- var currentTime = 0L;
-
- foreach (var (delayMs, costMs) in movements)
- {
- currentTime += delayMs;
- SetTickCount(currentTime);
-
- if (ns._lastMovementRecordTime > 0)
- {
- var interval = (int)(currentTime - ns._lastMovementRecordTime);
-
- // Skip chain-breaking gaps (> 2000ms) like the real code does
- if (interval > 2000)
- {
- ns._lastGapDuration = interval;
- ns._lastMovementRecordTime = currentTime;
- continue;
- }
-
- ns._movementHistory[ns._movementHistoryIndex] = new NetState.MovementRecord
- {
- Interval = (short)interval,
- TargetSpeed = (ushort)costMs,
- QueueDepth = 0,
- Flags = 0
- };
-
- ns._movementHistoryIndex++;
- if (ns._movementHistoryIndex >= 20)
- {
- ns._movementHistoryIndex = 0;
- ns._movementHistoryFull = true;
- }
- }
-
- ns._lastMovementRecordTime = currentTime;
- }
- }
-
- ///
- /// Scenario: Verifies that burst forgiveness triggers correctly and clears gap duration.
- ///
[Fact]
- public void MultipleLagSpikes_NoAccumulatedSuspicion()
+ public void AnalyzeMovement_BurstAfterGap_WrappedFullHistory_ForgivesAndClearsGap()
{
var ns = PacketTestUtilities.CreateTestNetState();
- SetTickCount(0);
- // Set up history buffer with mostly normal movements and a burst at the end
+ // The production recorder is private and only reachable through a full movement request, so
+ // the history is laid out directly: 17 normal steps, then a 3-step burst ending at the wrap
ns._movementHistory = new NetState.MovementRecord[20];
- // Fill with 17 normal movements (100ms intervals)
for (var i = 0; i < 17; i++)
{
ns._movementHistory[i] = new NetState.MovementRecord
@@ -98,7 +29,6 @@ public class MovementThrottleLatencyTests
};
}
- // Add 3 burst movements at the end (10ms intervals = burst)
for (var i = 17; i < 20; i++)
{
ns._movementHistory[i] = new NetState.MovementRecord
@@ -110,26 +40,19 @@ public class MovementThrottleLatencyTests
};
}
- ns._movementHistoryIndex = 0; // Next write position (wrapped)
- ns._movementHistoryFull = true; // Buffer is full
+ ns._movementHistoryIndex = 0;
+ ns._movementHistoryFull = true;
- // Set gap duration (simulating a lag spike before the burst)
+ // A lag spike longer than the chain gap preceded the burst
ns._lastGapDuration = 2500;
- // Analyze - burst forgiveness should trigger
- var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var samples, out var confidence);
+ var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out _, out var confidence);
- // Key assertions:
- // 1. Gap duration should be cleared (burst forgiveness triggered)
Assert.Equal(0, ns._lastGapDuration);
-
- // 2. Confidence should be reduced by 0.3x multiplier
Assert.True(
confidence < 0.5f,
$"Burst after gap should have low confidence. Confidence={confidence}, Rate={rate}"
);
-
- // 3. Should NOT be Definite despite the burst (forgiveness applied)
Assert.True(
verdict != MovementThrottle.DetectionVerdict.Definite,
$"Should not be Definite after burst forgiveness. Verdict={verdict}"
diff --git a/Projects/Server.Tests/Tests/Network/NetStateDisconnectTests.cs b/Projects/Server.Tests/Tests/Network/NetStateDisconnectTests.cs
index d1f273db3..fafb15d8a 100644
--- a/Projects/Server.Tests/Tests/Network/NetStateDisconnectTests.cs
+++ b/Projects/Server.Tests/Tests/Network/NetStateDisconnectTests.cs
@@ -1,6 +1,4 @@
using System;
-using System.Diagnostics;
-using System.Net.Sockets;
using Server.Network;
using Xunit;
@@ -96,35 +94,10 @@ public class NetStateDisconnectTests
Assert.True(ns._socket.DisconnectPending);
- var received = new byte[payload.Length];
- var total = 0;
- var deadline = Stopwatch.StartNew();
-
- while (total < payload.Length && deadline.ElapsedMilliseconds < 5000)
- {
- NetState.Slice();
- if (client.Poll(1000, SelectMode.SelectRead))
- {
- var read = client.Receive(received, total, payload.Length - total, SocketFlags.None);
- Assert.NotEqual(0, read);
- total += read;
- }
- }
-
- Assert.Equal(payload, received);
+ Assert.Equal(payload, NetTestHelpers.ReadAll(client, payload.Length, 5000));
// FIN follows once the send completes
- var eof = -1;
- while (eof != 0 && deadline.ElapsedMilliseconds < 5000)
- {
- NetState.Slice();
- if (client.Poll(1000, SelectMode.SelectRead))
- {
- eof = client.Receive(received);
- }
- }
-
- Assert.Equal(0, eof);
+ NetTestHelpers.WaitForEof(client, 5000);
}
finally
{
@@ -156,35 +129,8 @@ public class NetStateDisconnectTests
Assert.True(ns._socket.DisconnectPending);
- var received = new byte[payload.Length];
- var total = 0;
- var deadline = Stopwatch.StartNew();
-
- while (total < payload.Length && deadline.ElapsedMilliseconds < 10000)
- {
- NetState.Slice();
- if (client.Poll(1000, SelectMode.SelectRead))
- {
- var read = client.Receive(received, total, payload.Length - total, SocketFlags.None);
- Assert.NotEqual(0, read);
- total += read;
- }
- }
-
- Assert.Equal(payload.Length, total);
- Assert.Equal(payload, received);
-
- var eof = -1;
- while (eof != 0 && deadline.ElapsedMilliseconds < 10000)
- {
- NetState.Slice();
- if (client.Poll(1000, SelectMode.SelectRead))
- {
- eof = client.Receive(received);
- }
- }
-
- Assert.Equal(0, eof);
+ Assert.Equal(payload, NetTestHelpers.ReadAll(client, payload.Length));
+ NetTestHelpers.WaitForEof(client);
}
finally
{
diff --git a/Projects/Server.Tests/Tests/Network/NetStateRecvGuardTests.cs b/Projects/Server.Tests/Tests/Network/NetStateRecvGuardTests.cs
index 548406ec4..8d596d31b 100644
--- a/Projects/Server.Tests/Tests/Network/NetStateRecvGuardTests.cs
+++ b/Projects/Server.Tests/Tests/Network/NetStateRecvGuardTests.cs
@@ -1,8 +1,4 @@
-using System;
using System.Buffers;
-using System.Diagnostics;
-using System.Network;
-using System.Threading;
using Server.Network;
using Xunit;
@@ -12,71 +8,33 @@ namespace Server.Tests.Network;
public class NetStateRecvGuardTests
{
private const byte TestPacketId = 0xB1;
+ private const byte AttachAccountPacketId = 0x71;
- private static unsafe void RegisterVariableLength()
- {
- if (IncomingPackets.GetHandler(TestPacketId) == null)
- {
- IncomingPackets.Register(TestPacketId, 0, false, &NoOp);
- }
- }
+ private static unsafe NetTestHelpers.HandlerRegistration RegisterVariableLength() =>
+ NetTestHelpers.RegisterHandler(TestPacketId, 0, &NoOp);
private static void NoOp(NetState state, SpanReader reader)
{
}
- // Not 0x73: NetStateSendBufferTests registers its own no-op there first-come-wins, since the
- // handler table is process-global, and this test needs its own handler's side effect to fire.
- private const byte TestPingPacketId = 0x72;
-
- private static byte _lastPing;
-
- private static unsafe void RegisterNoOpPing()
- {
- if (IncomingPackets.GetHandler(TestPingPacketId) == null)
- {
- IncomingPackets.Register(TestPingPacketId, 2, false, &RecordPing);
- }
- }
-
- private static void RecordPing(NetState state, SpanReader reader) => _lastPing = reader.ReadByte();
-
- private const byte AttachAccountPacketId = 0x71;
-
- private static unsafe void RegisterAttachAccount()
- {
- if (IncomingPackets.GetHandler(AttachAccountPacketId) == null)
- {
- IncomingPackets.Register(AttachAccountPacketId, 3, false, &AttachAccount);
- }
- }
+ private static unsafe NetTestHelpers.HandlerRegistration RegisterAttachAccount() =>
+ NetTestHelpers.RegisterHandler(AttachAccountPacketId, 3, &AttachAccount);
// Stands in for the 0x91 handler: the account attaches mid-parse, so the promotion is pending
// with a receive armed when the next packet in the same completion reaches the guard
private static void AttachAccount(NetState state, SpanReader reader) => state.Account = new MockAccount();
- private static void SliceUntil(Func done)
- {
- var deadline = Stopwatch.StartNew();
- while (!done() && deadline.ElapsedMilliseconds < 5000)
- {
- NetState.Slice();
- Thread.Sleep(5);
- }
-
- Assert.True(done());
- }
-
[SkippableFact]
public void HandleReceive_PacketLongerThanTheRecvBuffer_IsAnError()
{
- RegisterVariableLength();
+ // A 16-bit length cannot exceed the 64 KiB base buffer, so the guard is only reachable
+ // through a pre-auth buffer
+ NetTestHelpers.RequirePreAuthBuffers();
+ using var handler = RegisterVariableLength();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
- // A 16-bit length cannot exceed a 64 KiB buffer, so the guard is unreachable there and
- // this only ever hit the `< 3` check on a buffer that size.
- Skip.If(ns._socket.RecvBuffer.PhysicalSize > ushort.MaxValue);
+ Assert.True(ns._socket.RecvBuffer.PhysicalSize <= ushort.MaxValue);
ns._protocolState = NetState.ProtocolState.GameServer_LoggedIn;
@@ -84,7 +42,7 @@ public class NetStateRecvGuardTests
// park the connection with nothing to arm a recv into
var declared = ns._socket.RecvBuffer.PhysicalSize; // one more than the buffer can ever hold
client.Send(new byte[] { TestPacketId, (byte)(declared >> 8), (byte)declared });
- SliceUntil(() => ns._protocolState == NetState.ProtocolState.Error);
+ NetTestHelpers.SliceUntil(() => ns._protocolState == NetState.ProtocolState.Error);
Assert.Contains("bad state", ns._disconnectReason);
}
@@ -98,7 +56,7 @@ public class NetStateRecvGuardTests
[Fact]
public void HandleReceive_PacketThatFits_WaitsForTheRest()
{
- RegisterVariableLength();
+ using var handler = RegisterVariableLength();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -106,7 +64,7 @@ public class NetStateRecvGuardTests
var declared = ns._socket.RecvBuffer.PhysicalSize / 2;
client.Send(new byte[] { TestPacketId, (byte)(declared >> 8), (byte)declared });
- SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 3);
+ NetTestHelpers.SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 3);
Assert.Equal(NetState.ProtocolState.GameServer_LoggedIn, ns._protocolState);
Assert.True(ns.Running);
@@ -122,8 +80,8 @@ public class NetStateRecvGuardTests
[SkippableFact]
public void HandleReceive_OversizePacket_WithAnAccount_RetriesPromotionInsteadOfErroring()
{
- Skip.If(NetState.InitialRecvBufferSize == 0);
- RegisterVariableLength();
+ NetTestHelpers.RequirePreAuthBuffers();
+ using var handler = RegisterVariableLength();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -135,7 +93,7 @@ public class NetStateRecvGuardTests
var declared = NetState.InitialRecvBufferSize; // as much as the initial buffer can ever hold
client.Send(new byte[] { TestPacketId, (byte)(declared >> 8), (byte)declared });
- SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 3);
+ NetTestHelpers.SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 3);
Assert.Equal(NetState.ProtocolState.GameServer_LoggedIn, ns._protocolState);
Assert.True(ns.Running);
@@ -143,7 +101,7 @@ public class NetStateRecvGuardTests
// The deferred promotion applies at this next completion; the header is then delivered
// whole to the 0xB1 no-op handler
client.Send(new byte[declared - 3]);
- SliceUntil(
+ NetTestHelpers.SliceUntil(
() => ns._socket.RecvBuffer.ReadableBytes == 0 &&
ns._socket.RecvBuffer.PhysicalSize == NetState.RecvBufferSize
);
@@ -161,8 +119,8 @@ public class NetStateRecvGuardTests
[SkippableFact]
public void HandleReceive_AfterABurstFillsTheInitialBuffer_KeepsReceiving()
{
- Skip.If(NetState.InitialRecvBufferSize == 0);
- RegisterNoOpPing();
+ NetTestHelpers.RequirePreAuthBuffers();
+ using var ping = NetTestHelpers.RegisterPing();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -174,17 +132,17 @@ public class NetStateRecvGuardTests
var pings = new byte[capacity + 1];
for (var i = 0; i < pings.Length; i += 2)
{
- pings[i] = TestPingPacketId;
+ pings[i] = NetTestHelpers.PingPacketId;
pings[i + 1] = (byte)(i / 2);
}
client.Send(pings);
- SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 0 && ns._receivedData);
+ NetTestHelpers.SliceUntil(() => ns._socket.RecvBuffer.ReadableBytes == 0 && ns._receivedData);
Assert.True(ns.Running);
// Without a re-arm this ping never arrives
- client.Send(new byte[] { TestPingPacketId, 0xEE });
- SliceUntil(() => _lastPing == 0xEE);
+ client.Send(new byte[] { NetTestHelpers.PingPacketId, 0xEE });
+ NetTestHelpers.SliceUntil(() => NetTestHelpers.LastPing == 0xEE);
Assert.True(ns.Running);
}
finally
@@ -197,9 +155,9 @@ public class NetStateRecvGuardTests
[SkippableFact]
public void HandleReceive_AccountAttachedMidParse_ThenOversizeHeader_WaitsForThePendingPromotion()
{
- Skip.If(NetState.InitialRecvBufferSize == 0);
- RegisterAttachAccount();
- RegisterVariableLength();
+ NetTestHelpers.RequirePreAuthBuffers();
+ using var attach = RegisterAttachAccount();
+ using var handler = RegisterVariableLength();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -212,7 +170,7 @@ public class NetStateRecvGuardTests
// header then hits the guard after the promotion already applied and simply waits on the
// 64 KiB buffer, so the test cannot flake, it just exercises the weaker path on that run.
client.Send(new byte[] { AttachAccountPacketId, 0x00, 0x00, TestPacketId, (byte)(declared >> 8), (byte)declared });
- SliceUntil(() => ns.Account != null);
+ NetTestHelpers.SliceUntil(() => ns.Account != null);
Assert.True(ns.Running);
Assert.Equal(NetState.ProtocolState.GameServer_LoggedIn, ns._protocolState);
@@ -220,7 +178,7 @@ public class NetStateRecvGuardTests
// The next completion applies the promotion and the whole packet is delivered
client.Send(new byte[declared - 3]);
- SliceUntil(
+ NetTestHelpers.SliceUntil(
() => ns._socket.RecvBuffer.ReadableBytes == 0 &&
ns._socket.RecvBuffer.PhysicalSize == NetState.RecvBufferSize
);
diff --git a/Projects/Server.Tests/Tests/Network/NetStateSendBufferTests.cs b/Projects/Server.Tests/Tests/Network/NetStateSendBufferTests.cs
index 6f5a22d04..c92260070 100644
--- a/Projects/Server.Tests/Tests/Network/NetStateSendBufferTests.cs
+++ b/Projects/Server.Tests/Tests/Network/NetStateSendBufferTests.cs
@@ -1,10 +1,8 @@
using System;
using System.Buffers;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Net.Sockets;
using System.Network;
-using System.Threading;
using Server.Network;
using Xunit;
@@ -22,42 +20,11 @@ public class NetStateSendBufferTests
return ns;
}
- private static byte[] Pattern(int length, int seed)
- {
- var data = new byte[length];
- new System.Random(seed).NextBytes(data);
- return data;
- }
-
- private static unsafe void RegisterNoOpPing()
- {
- if (IncomingPackets.GetHandler(0x73) == null)
- {
- IncomingPackets.Register(0x73, 2, false, &NoOp);
- }
- }
-
- private static void NoOp(NetState state, SpanReader reader)
- {
- }
-
- private static void SliceUntil(Func done)
- {
- var deadline = Stopwatch.StartNew();
- while (!done() && deadline.ElapsedMilliseconds < 5000)
- {
- NetState.Slice();
- Thread.Sleep(5);
- }
-
- Assert.True(done());
- }
-
[SkippableFact]
public void Account_InTheGameServerState_PromotesBothBuffers()
{
- Skip.If(NetState.InitialSendBufferSize == 0);
- RegisterNoOpPing();
+ NetTestHelpers.RequirePreAuthBuffers();
+ using var ping = NetTestHelpers.RegisterPing();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -73,8 +40,8 @@ public class NetStateSendBufferTests
// Recv promotes at the next completion
ns._protocolState = NetState.ProtocolState.GameServer_LoggedIn;
- client.Send(new byte[] { 0x73, 0x01 });
- SliceUntil(() => ns._socket.RecvBuffer.PhysicalSize == NetState.RecvBufferSize);
+ client.Send(new byte[] { NetTestHelpers.PingPacketId, 0x01 });
+ NetTestHelpers.SliceUntil(() => ns._socket.RecvBuffer.PhysicalSize == NetState.RecvBufferSize);
Assert.True(ns.Running);
}
finally
@@ -87,7 +54,7 @@ public class NetStateSendBufferTests
[SkippableFact]
public void Account_OnTheLoginServer_KeepsTheInitialBuffers()
{
- Skip.If(NetState.InitialSendBufferSize == 0);
+ NetTestHelpers.RequirePreAuthBuffers();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -108,11 +75,11 @@ public class NetStateSendBufferTests
public void Send_BeyondTheInitialBuffer_BeforeCredentials_IsExhausted()
{
// Nothing promotes before credentials verify: the 4 KiB ring is the whole pre-auth budget
- Skip.If(NetState.InitialSendBufferSize == 0);
+ NetTestHelpers.RequirePreAuthBuffers();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
- var data = Pattern(NetState.InitialSendBufferSize + 512, 7);
+ var data = NetTestHelpers.Pattern(NetState.InitialSendBufferSize + 512, 7);
ns.Send(data);
Assert.Contains("exhausted", ns._disconnectReason, StringComparison.OrdinalIgnoreCase);
@@ -123,7 +90,7 @@ public class NetStateSendBufferTests
// Once it completes the NetState is disposed and _socket goes null, so nothing below
// this point may touch it.
client.Close();
- SliceUntil(() => !ns.Running);
+ NetTestHelpers.SliceUntil(() => !ns.Running);
}
finally
{
@@ -137,19 +104,19 @@ public class NetStateSendBufferTests
{
// The late-verdict path from Account_AssignedAfterTheStateFlipped_StillPromotes: an account
// attached while the setter's own gate does not fire must still let the send path promote
- Skip.If(NetState.InitialSendBufferSize == 0);
+ NetTestHelpers.RequirePreAuthBuffers();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
ns.Account = new MockAccount(); // _protocolState is still AwaitingSeed: the setter does not promote
- var data = Pattern(NetState.InitialSendBufferSize + 512, 7);
+ var data = NetTestHelpers.Pattern(NetState.InitialSendBufferSize + 512, 7);
ns.Send(data);
Assert.True(ns.Running);
Assert.Equal(string.Empty, ns._disconnectReason);
Assert.Equal(NetState.SendBufferSize, ns._socket.SendBuffer.PhysicalSize);
Assert.False(ns._sendBufferGrown); // promotion is not growth: nothing to shrink later
- Assert.Equal(data, ReadAll(client, data.Length));
+ Assert.Equal(data, NetTestHelpers.ReadAll(client, data.Length));
}
finally
{
@@ -161,12 +128,12 @@ public class NetStateSendBufferTests
[SkippableFact]
public void Send_BacklogBeforeCredentials_IsExhaustedInsteadOfPromoted()
{
- Skip.If(NetState.InitialSendBufferSize == 0);
+ NetTestHelpers.RequirePreAuthBuffers();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
// No Slice() between sends: nothing drains, so a second write finds a non-empty initial buffer
- var half = Pattern(NetState.InitialSendBufferSize / 2, 11);
+ var half = NetTestHelpers.Pattern(NetState.InitialSendBufferSize / 2, 11);
ns.Send(half);
ns.Send(half);
ns.Send(half); // cannot fit, buffer not empty, no account: refused
@@ -179,7 +146,7 @@ public class NetStateSendBufferTests
// Once it completes the NetState is disposed and _socket goes null, so nothing below
// this point may touch it.
client.Close();
- SliceUntil(() => !ns.Running);
+ NetTestHelpers.SliceUntil(() => !ns.Running);
}
finally
{
@@ -191,7 +158,7 @@ public class NetStateSendBufferTests
[SkippableFact]
public void Account_AssignedAfterTheStateFlipped_StillPromotes()
{
- Skip.If(NetState.InitialSendBufferSize == 0);
+ NetTestHelpers.RequirePreAuthBuffers();
var ns = PacketTestUtilities.CreateTestNetState(out var client);
try
{
@@ -211,7 +178,7 @@ public class NetStateSendBufferTests
{
var randomLength = compressedLength * 5 / 8;
var data = new byte[compressedLength * 8];
- Pattern(randomLength, seed).CopyTo(data, 0);
+ NetTestHelpers.Pattern(randomLength, seed).CopyTo(data, 0);
var scratch = new byte[compressedLength * 2 + 16];
for (var length = randomLength; length < data.Length; length++)
@@ -228,47 +195,12 @@ public class NetStateSendBufferTests
throw new InvalidOperationException($"No chunk compresses to exactly {compressedLength} bytes");
}
- private static byte[] ReadAll(Socket client, int length)
- {
- var received = new byte[length];
- var total = 0;
- var deadline = Stopwatch.StartNew();
- while (total < length && deadline.ElapsedMilliseconds < 10000)
- {
- NetState.Slice();
-
- // Poll takes microseconds, not milliseconds
- if (client.Poll(50_000, SelectMode.SelectRead))
- {
- var read = client.Receive(received, total, length - total, SocketFlags.None);
- Assert.NotEqual(0, read);
- total += read;
- }
- }
-
- Assert.Equal(length, total);
- return received;
- }
-
// Peer receipt does not free the buffer; the completion must land first
- private static void WaitForDrain(NetState ns)
- {
- var deadline = Stopwatch.StartNew();
- while (deadline.ElapsedMilliseconds < 10000)
- {
- NetState.Slice();
-
- if (ns._socket.SendBuffer.ReadableBytes == 0 && ns._socket.SendBuffer.InFlightBytes == 0)
- {
- break;
- }
-
- Thread.Sleep(5);
- }
-
- Assert.Equal(0, ns._socket.SendBuffer.ReadableBytes);
- Assert.Equal(0, ns._socket.SendBuffer.InFlightBytes);
- }
+ private static void WaitForDrain(NetState ns) =>
+ NetTestHelpers.SliceUntil(
+ () => ns._socket.SendBuffer.ReadableBytes == 0 && ns._socket.SendBuffer.InFlightBytes == 0,
+ 10000
+ );
[Fact]
public void Send_GrowsInsteadOfDisconnecting_AndStreamStaysIntact()
@@ -283,7 +215,7 @@ public class NetStateSendBufferTests
var expected = new byte[chunk * 6];
for (var i = 0; i < 6; i++)
{
- var data = Pattern(chunk, i);
+ var data = NetTestHelpers.Pattern(chunk, i);
data.CopyTo(expected, i * chunk);
ns.Send(data);
}
@@ -293,7 +225,7 @@ public class NetStateSendBufferTests
Assert.True(ns._socket.SendBuffer.PhysicalSize > baseSize);
Assert.True(ns._sendBufferGrown);
- Assert.Equal(expected, ReadAll(client, expected.Length));
+ Assert.Equal(expected, NetTestHelpers.ReadAll(client, expected.Length));
}
finally
{
@@ -318,7 +250,7 @@ public class NetStateSendBufferTests
var expectedLength = 0;
for (var i = 0; i < 6; i++)
{
- var data = Pattern(chunkLength, i + 100);
+ var data = NetTestHelpers.Pattern(chunkLength, i + 100);
var compressedLength = NetworkCompression.Compress(data, scratch);
Assert.True(compressedLength > 0);
scratch.AsSpan(0, compressedLength).CopyTo(expected.AsSpan(expectedLength));
@@ -332,7 +264,7 @@ public class NetStateSendBufferTests
Assert.True(ns._sendBufferGrown);
Array.Resize(ref expected, expectedLength);
- Assert.Equal(expected, ReadAll(client, expected.Length));
+ Assert.Equal(expected, NetTestHelpers.ReadAll(client, expected.Length));
}
finally
{
@@ -365,7 +297,7 @@ public class NetStateSendBufferTests
while (ns._socket.SendBuffer.WritableBytes > 4096)
{
var writable = ns._socket.SendBuffer.WritableBytes;
- SendAndRecord(Pattern(Math.Min(baseSize / 8, (writable - 2048) * 8 / 11), seed++));
+ SendAndRecord(NetTestHelpers.Pattern(Math.Min(baseSize / 8, (writable - 2048) * 8 / 11), seed++));
}
// Full; no span to compress into
@@ -381,7 +313,7 @@ public class NetStateSendBufferTests
Assert.Equal(baseSize * 2, ns._socket.SendBuffer.PhysicalSize);
Assert.True(ns._sendBufferGrown);
- Assert.Equal(expected.ToArray(), ReadAll(client, expected.Count));
+ Assert.Equal(expected.ToArray(), NetTestHelpers.ReadAll(client, expected.Count));
}
finally
{
@@ -401,8 +333,8 @@ public class NetStateSendBufferTests
// A small peer buffer keeps the first send in flight through the growth
client.ReceiveBufferSize = 4096;
- var first = Pattern(baseSize / 2, 300);
- var second = Pattern(baseSize / 2 + 1, 301);
+ var first = NetTestHelpers.Pattern(baseSize / 2, 300);
+ var second = NetTestHelpers.Pattern(baseSize / 2 + 1, 301);
ns.Send(first);
NetState.Slice(); // posts the first send
@@ -419,7 +351,7 @@ public class NetStateSendBufferTests
first.CopyTo(expected, 0);
second.CopyTo(expected, first.Length);
- Assert.Equal(expected, ReadAll(client, expected.Length));
+ Assert.Equal(expected, NetTestHelpers.ReadAll(client, expected.Length));
}
finally
{
@@ -439,11 +371,11 @@ public class NetStateSendBufferTests
var chunk = baseSize / 4;
for (var i = 0; i < 6; i++)
{
- ns.Send(Pattern(chunk, i));
+ ns.Send(NetTestHelpers.Pattern(chunk, i));
}
Assert.True(ns._sendBufferGrown);
- ReadAll(client, chunk * 6);
+ NetTestHelpers.ReadAll(client, chunk * 6);
WaitForDrain(ns);
var grewAt = ns._sendBufferGrewAt;
@@ -471,11 +403,11 @@ public class NetStateSendBufferTests
var chunk = baseSize / 4;
for (var i = 0; i < 6; i++)
{
- ns.Send(Pattern(chunk, i + 400));
+ ns.Send(NetTestHelpers.Pattern(chunk, i + 400));
}
Assert.True(ns._sendBufferGrown);
- ReadAll(client, chunk * 6);
+ NetTestHelpers.ReadAll(client, chunk * 6);
WaitForDrain(ns);
// The sweep shrinks; nothing here calls TryShrinkSendBuffer
@@ -504,7 +436,7 @@ public class NetStateSendBufferTests
var writes = NetState.MaxSendBufferSize / chunk + 1;
for (var i = 0; i < writes; i++)
{
- ns.Send(Pattern(chunk, i));
+ ns.Send(NetTestHelpers.Pattern(chunk, i));
}
Assert.Contains("Send buffer exhausted", ns._disconnectReason);
@@ -532,7 +464,7 @@ public class NetStateSendBufferTests
for (var i = 0; i < 6; i++)
{
- ns.Send(Pattern(baseSize / 4, i));
+ ns.Send(NetTestHelpers.Pattern(baseSize / 4, i));
}
Assert.Equal(baseSize, ns._socket.SendBuffer.PhysicalSize);
@@ -558,7 +490,7 @@ public class NetStateSendBufferTests
NetState.Slice(); // handoff
// CannotSendPackets short-circuits before any growth
- ns.Send(Pattern(baseSize * 2, 500));
+ ns.Send(NetTestHelpers.Pattern(baseSize * 2, 500));
Assert.Equal(baseSize, ns._socket.SendBuffer.PhysicalSize);
Assert.False(ns._sendBufferGrown);
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs
index 6d8bd9fd9..c6a8b43a4 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs
@@ -1,7 +1,5 @@
using System;
-using System.Collections.Generic;
using System.Net;
-using Server.Accounting;
using Server.Network;
using Xunit;
@@ -10,60 +8,6 @@ namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
public class AccountPacketTests
{
- private class MockedAccount : IAccount
- {
- public int TotalGold { get; }
- public int TotalPlat { get; }
- public bool DepositGold(int amount) => throw new NotImplementedException();
-
- public bool DepositPlat(int amount) => throw new NotImplementedException();
-
- public bool WithdrawGold(int amount) => throw new NotImplementedException();
-
- public bool WithdrawPlat(int amount) => throw new NotImplementedException();
-
- public long GetTotalGold() => throw new NotImplementedException();
-
- public int CompareTo(IAccount other) => throw new NotImplementedException();
-
- public string Username { get; }
- public string Email { get; set; }
- public AccessLevel AccessLevel { get; set; }
- public int Length { get; }
- public int Limit { get; }
- public int Count { get; }
-
- private Dictionary _mobiles = new();
-
- public Mobile this[int index]
- {
- get => _mobiles[index];
- set
- {
- _mobiles[index] = value;
- if (value != null)
- {
- value.Account = this;
- }
- }
- }
-
- public DateTime Created { get; set; }
- public Serial Serial { get; }
- public void Deserialize(IGenericReader reader) => throw new NotImplementedException();
-
- public void Serialize(IGenericWriter writer) => throw new NotImplementedException();
-
- public bool Deleted { get; }
-
- public void Delete() => throw new NotImplementedException();
- public bool TrySetUsername(string username) => throw new NotImplementedException();
-
- public void SetPassword(string password) => throw new NotImplementedException();
-
- public bool CheckPassword(string password) => throw new NotImplementedException();
- }
-
[Fact]
public void TestChangeCharacter()
{
@@ -75,58 +19,38 @@ public class AccountPacketTests
secondMobile.DefaultMobileInit();
secondMobile.RawName = null;
- var account = new MockedAccount
+ var account = new MockAccount
{
[0] = firstMobile,
[1] = null,
[2] = secondMobile
};
- // var account = new MockAccount(new[] { firstMobile, null, secondMobile });
- var expected = new ChangeCharacter(account).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendChangeCharacter(account);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ChangeCharacter(account), ns => ns.SendChangeCharacter(account));
}
[Fact]
public void TestClientVersionReq()
{
- var expected = new ClientVersionReq().Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
-
- ns.SendClientVersionRequest();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ClientVersionReq(), ns => ns.SendClientVersionRequest());
}
[Fact]
public void TestDeleteResult()
{
- var expected = new DeleteResult(DeleteResultType.BadRequest).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCharacterDeleteResult(DeleteResultType.BadRequest);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DeleteResult(DeleteResultType.BadRequest),
+ ns => ns.SendCharacterDeleteResult(DeleteResultType.BadRequest)
+ );
}
[Fact]
public void TestPopupMessage()
{
- var expected = new PopupMessage(PMMessage.LoginSyncError).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPopupMessage(PMMessage.LoginSyncError);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new PopupMessage(PMMessage.LoginSyncError),
+ ns => ns.SendPopupMessage(PMMessage.LoginSyncError)
+ );
}
[Theory, InlineData(ProtocolChanges.Version70610), InlineData(ProtocolChanges.Version6000)]
@@ -136,7 +60,7 @@ public class AccountPacketTests
firstMobile.DefaultMobileInit();
firstMobile.Name = "Test Mobile";
- var account = new MockedAccount
+ var account = new MockAccount
{
[0] = firstMobile,
[1] = null,
@@ -168,25 +92,13 @@ public class AccountPacketTests
m.Direction = Direction.Down;
m.LogoutMap = Map.Felucca;
- var expected = new LoginConfirm(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendLoginConfirmation(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new LoginConfirm(m), ns => ns.SendLoginConfirmation(m));
}
[Fact]
public void TestLoginComplete()
{
- var expected = new LoginComplete().Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendLoginComplete();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new LoginComplete(), ns => ns.SendLoginComplete());
}
[Fact]
@@ -196,7 +108,7 @@ public class AccountPacketTests
firstMobile.DefaultMobileInit();
firstMobile.RawName = "Test Mobile";
- var account = new MockedAccount
+ var account = new MockAccount
{
[0] = null,
[1] = firstMobile,
@@ -205,13 +117,7 @@ public class AccountPacketTests
[4] = null
};
- var expected = new CharacterListUpdate(account).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCharacterListUpdate(account);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new CharacterListUpdate(account), ns => ns.SendCharacterListUpdate(account));
}
[Fact]
@@ -221,7 +127,7 @@ public class AccountPacketTests
firstMobile.DefaultMobileInit();
firstMobile.Name = "Test Mobile";
- var account = new MockedAccount
+ var account = new MockAccount
{
[0] = null,
[1] = firstMobile,
@@ -235,17 +141,16 @@ public class AccountPacketTests
new CityInfo("Test City", "Test Building", 50, 100, 10, -10)
};
- var expected = new CharacterList(account, info).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.CityInfo = info;
- ns.Account = account;
- ns.ProtocolChanges = ProtocolChanges.Version70130;
-
- ns.SendCharacterList();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new CharacterList(account, info),
+ ns =>
+ {
+ ns.CityInfo = info;
+ ns.Account = account;
+ ns.SendCharacterList();
+ },
+ ProtocolChanges.Version70130
+ );
}
[Fact]
@@ -255,7 +160,7 @@ public class AccountPacketTests
firstMobile.DefaultMobileInit();
firstMobile.Name = "Test Mobile";
- var account = new MockedAccount
+ var account = new MockAccount
{
[0] = null,
[1] = firstMobile,
@@ -269,29 +174,23 @@ public class AccountPacketTests
new CityInfo("Test City", "Test Building", 50, 100, 10, -10)
};
- var expected = new CharacterListOld(account, info).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.CityInfo = info;
- ns.Account = account;
-
- ns.SendCharacterList();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new CharacterListOld(account, info),
+ ns =>
+ {
+ ns.CityInfo = info;
+ ns.Account = account;
+ ns.SendCharacterList();
+ }
+ );
}
[Fact]
public void TestAccountLoginRej()
{
var reason = ALRReason.BadComm;
- var expected = new AccountLoginRej(reason).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendAccountLoginRejected(reason);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new AccountLoginRej(reason), ns => ns.SendAccountLoginRejected(reason));
}
[Fact]
@@ -302,15 +201,14 @@ public class AccountPacketTests
new ServerInfo("Test Server", 0, TimeZoneInfo.Local, IPEndPoint.Parse("127.0.0.1"))
};
- var expected = new AccountLoginAck(info).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ServerInfo = info;
-
- ns.SendAccountLoginAck();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new AccountLoginAck(info),
+ ns =>
+ {
+ ns.ServerInfo = info;
+ ns.SendAccountLoginAck();
+ }
+ );
}
[Fact]
@@ -319,13 +217,6 @@ public class AccountPacketTests
var si = new ServerInfo("Test Server", 0, TimeZoneInfo.Local, IPEndPoint.Parse("127.0.0.1"));
const int authId = 0x123456;
- var expected = new PlayServerAck(si, authId).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
-
- ns.SendPlayServerAck(si, authId);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new PlayServerAck(si, authId), ns => ns.SendPlayServerAck(si, authId));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/CombatPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/CombatPacketTests.cs
index a885cae9e..de2401a0c 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/CombatPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/CombatPacketTests.cs
@@ -12,25 +12,13 @@ public class CombatPacketTests
var attacker = (Serial)0x1024;
var defender = (Serial)0x2048;
- var expected = new Swing(attacker, defender).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSwing(attacker, defender);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new Swing(attacker, defender), ns => ns.SendSwing(attacker, defender));
}
[Theory, InlineData(true), InlineData(false)]
public void TestSetWarMode(bool warmode)
{
- var expected = new SetWarMode(warmode).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSetWarMode(warmode);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new SetWarMode(warmode), ns => ns.SendSetWarMode(warmode));
}
[Fact]
@@ -38,12 +26,6 @@ public class CombatPacketTests
{
var serial = (Serial)0x1024;
- var expected = new ChangeCombatant(serial).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendChangeCombatant(serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ChangeCombatant(serial), ns => ns.SendChangeCombatant(serial));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs
index 00dfc4054..98229c6fe 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs
@@ -14,13 +14,7 @@ public class ContainerPacketTests
var serial = (Serial)0x1024;
ushort gumpId = 100;
- var expected = new ContainerDisplay(serial, gumpId).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayContainer(serial, gumpId);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ContainerDisplay(serial, gumpId), ns => ns.SendDisplayContainer(serial, gumpId));
}
[Fact]
@@ -29,14 +23,11 @@ public class ContainerPacketTests
var serial = (Serial)0x1024;
ushort gumpId = 100;
- var expected = new ContainerDisplayHS(serial, gumpId).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ns.ProtocolChanges | ProtocolChanges.ContainerGridLines | ProtocolChanges.HighSeas;
- ns.SendDisplayContainer(serial, gumpId);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new ContainerDisplayHS(serial, gumpId),
+ ns => ns.SendDisplayContainer(serial, gumpId),
+ ProtocolChanges.ContainerGridLines | ProtocolChanges.HighSeas
+ );
}
[Fact]
@@ -44,13 +35,7 @@ public class ContainerPacketTests
{
var serial = (Serial)0x1024;
- var expected = new DisplaySpellbook(serial).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplaySpellbook(serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DisplaySpellbook(serial), ns => ns.SendDisplaySpellbook(serial));
}
[Fact]
@@ -58,14 +43,11 @@ public class ContainerPacketTests
{
var serial = (Serial)0x1024;
- var expected = new DisplaySpellbookHS(serial).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ns.ProtocolChanges | ProtocolChanges.ContainerGridLines | ProtocolChanges.HighSeas;
- ns.SendDisplaySpellbook(serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplaySpellbookHS(serial),
+ ns => ns.SendDisplaySpellbook(serial),
+ ProtocolChanges.ContainerGridLines | ProtocolChanges.HighSeas
+ );
}
[Fact]
@@ -76,17 +58,20 @@ public class ContainerPacketTests
ushort offset = 10;
ulong content = 0x123456789ABCDEF0;
var opl = ObjectPropertyList.Enabled;
-
- var expected = new NewSpellbookContent(serial, graphic, offset, content).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ns.ProtocolChanges | ProtocolChanges.ContainerGridLines | ProtocolChanges.NewSpellbook;
ObjectPropertyList.Enabled = true;
- ns.SendSpellbookContent(serial, graphic, offset, content);
- ObjectPropertyList.Enabled = opl;
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ try
+ {
+ PacketAssert.Sends(
+ new NewSpellbookContent(serial, graphic, offset, content),
+ ns => ns.SendSpellbookContent(serial, graphic, offset, content),
+ ProtocolChanges.ContainerGridLines | ProtocolChanges.NewSpellbook
+ );
+ }
+ finally
+ {
+ ObjectPropertyList.Enabled = opl;
+ }
}
[Fact]
@@ -97,13 +82,10 @@ public class ContainerPacketTests
ushort graphic = 100;
ulong content = 0x123456789ABCDEF0;
- var expected = new SpellbookContent(serial, offset, content).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSpellbookContent(serial, graphic, offset, content);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new SpellbookContent(serial, offset, content),
+ ns => ns.SendSpellbookContent(serial, graphic, offset, content)
+ );
}
[Fact]
@@ -114,14 +96,11 @@ public class ContainerPacketTests
ushort graphic = 100;
ulong content = 0x123456789ABCDEF0;
- var expected = new SpellbookContent6017(serial, offset, content).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
- ns.SendSpellbookContent(serial, graphic, offset, content);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new SpellbookContent6017(serial, offset, content),
+ ns => ns.SendSpellbookContent(serial, graphic, offset, content),
+ ProtocolChanges.ContainerGridLines
+ );
}
[Fact]
@@ -130,13 +109,7 @@ public class ContainerPacketTests
var serial = (Serial)0x1024;
var item = new Item(serial);
- var expected = new ContainerContentUpdate(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendContainerContentUpdate(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ContainerContentUpdate(item), ns => ns.SendContainerContentUpdate(item));
}
[Fact]
@@ -145,66 +118,17 @@ public class ContainerPacketTests
var serial = (Serial)0x1024;
var item = new Item(serial);
- var expected = new ContainerContentUpdate6017(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
- ns.SendContainerContentUpdate(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new ContainerContentUpdate6017(item),
+ ns => ns.SendContainerContentUpdate(item),
+ ProtocolChanges.ContainerGridLines
+ );
}
- [Fact]
- public void TestContainerContent()
+ // An administrator beholder on the container's map, so every item is visible and sent.
+ private static (Container Container, Mobile Beholder) CreateViewedContainer(int itemCount)
{
var cont = new Container(World.NewItem);
- cont.AddItem(new Item(World.NewItem));
- cont.Map = Map.Felucca;
-
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.AccessLevel = AccessLevel.Administrator;
- m.Map = Map.Felucca;
-
- var expected = new ContainerContent(m, cont).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendContainerContent(m, cont);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
- }
-
- [Fact]
- public void TestContainerContent6017()
- {
- var cont = new Container(World.NewItem);
- cont.AddItem(new Item(World.NewItem));
- cont.Map = Map.Felucca;
-
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.AccessLevel = AccessLevel.Administrator;
- m.Map = Map.Felucca;
-
- var expected = new ContainerContent6017(m, cont).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
- ns.SendContainerContent(m, cont);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
- }
-
- [Fact]
- public void TestContainerContent_CapsAtProtocolLimit()
- {
- var cont = new Container(World.NewItem);
-
- // Past the 3448-entry cap (19-byte entries) so truncation engages.
- const int itemCount = 5000;
for (var i = 0; i < itemCount; i++)
{
cont.AddItem(new Item(World.NewItem));
@@ -217,9 +141,62 @@ public class ContainerPacketTests
m.AccessLevel = AccessLevel.Administrator;
m.Map = Map.Felucca;
+ return (cont, m);
+ }
+
+ [Fact]
+ public void TestContainerContent()
+ {
+ var (cont, m) = CreateViewedContainer(1);
+
+ try
+ {
+ PacketAssert.Sends(new ContainerContent(m, cont), ns => ns.SendContainerContent(m, cont));
+ }
+ finally
+ {
+ cont.Delete();
+ m.Delete();
+ }
+ }
+
+ [Fact]
+ public void TestContainerContent6017()
+ {
+ var (cont, m) = CreateViewedContainer(1);
+
+ try
+ {
+ PacketAssert.Sends(
+ new ContainerContent6017(m, cont),
+ ns => ns.SendContainerContent(m, cont),
+ ProtocolChanges.ContainerGridLines
+ );
+ }
+ finally
+ {
+ cont.Delete();
+ m.Delete();
+ }
+ }
+
+ // 0x3C's length and count fields are 16-bit: 19-byte entries cap at 3448, 20-byte grid-line entries at 3276.
+ [Theory]
+ [InlineData(false, 3448, 65517)]
+ [InlineData(true, 3276, 65525)]
+ public void TestContainerContent_CapsAtProtocolLimit(bool gridLines, int maxEntries, int maxLength)
+ {
+ // One item past the cap is enough to make truncation engage.
+ var (cont, m) = CreateViewedContainer(maxEntries + 1);
+
try
{
using var ns = PacketTestUtilities.CreateTestNetState();
+ if (gridLines)
+ {
+ ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
+ }
+
// A ~64KB packet exceeds the 4KB pre-auth send buffer; an account lets Send() promote it
// on demand instead of exhausting the buffer and disconnecting.
ns.Account = new MockAccount();
@@ -233,57 +210,9 @@ public class ContainerPacketTests
var length = BinaryPrimitives.ReadUInt16BigEndian(span[1..3]);
var count = BinaryPrimitives.ReadUInt16BigEndian(span[3..5]);
- const int entrySize = 19; // no grid lines negotiated on this NetState
- var expectedMaxEntries = (ushort.MaxValue - 5) / entrySize;
-
- Assert.Equal(65517, length);
+ Assert.Equal(maxLength, length);
Assert.Equal(length, span.Length);
- Assert.Equal(expectedMaxEntries, count);
- }
- finally
- {
- cont.Delete();
- m.Delete();
- }
- }
-
- [Fact]
- public void TestContainerContent_CapsAtProtocolLimit_WithGridLines()
- {
- var cont = new Container(World.NewItem);
-
- // Past the 3276-entry cap (20-byte entries with grid lines) so truncation engages.
- const int itemCount = 5000;
- for (var i = 0; i < itemCount; i++)
- {
- cont.AddItem(new Item(World.NewItem));
- }
-
- cont.Map = Map.Felucca;
-
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.AccessLevel = AccessLevel.Administrator;
- m.Map = Map.Felucca;
-
- try
- {
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
- ns.Account = new MockAccount();
-
- var ex = Record.Exception(() => ns.SendContainerContent(m, cont));
- Assert.Null(ex);
-
- var span = ns.SendBuffer.GetReadSpan();
- Assert.Equal((byte)0x3C, span[0]);
-
- var length = BinaryPrimitives.ReadUInt16BigEndian(span[1..3]);
- var count = BinaryPrimitives.ReadUInt16BigEndian(span[3..5]);
-
- Assert.Equal(65525, length);
- Assert.Equal(length, span.Length);
- Assert.Equal(3276, count);
+ Assert.Equal(maxEntries, count);
}
finally
{
@@ -295,32 +224,13 @@ public class ContainerPacketTests
[Fact]
public void TestContainerContent_RentedBuffer_MatchesReferencePacket()
{
- var cont = new Container(World.NewItem);
-
// Below the entry cap but past the 1024-byte stackalloc threshold, so SendContainerContent
// rents its buffer instead of using the stack.
- const int itemCount = 100;
- for (var i = 0; i < itemCount; i++)
- {
- cont.AddItem(new Item(World.NewItem));
- }
-
- cont.Map = Map.Felucca;
-
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.AccessLevel = AccessLevel.Administrator;
- m.Map = Map.Felucca;
+ var (cont, m) = CreateViewedContainer(100);
try
{
- var expected = new ContainerContent(m, cont).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendContainerContent(m, cont);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ContainerContent(m, cont), ns => ns.SendContainerContent(m, cont));
}
finally
{
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs
index 5f31a49d4..bda7ad009 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs
@@ -11,13 +11,7 @@ public class DamagePacketTests
{
var serial = (Serial)0x1024;
- var expected = new DamagePacketOld(serial, inputAmount).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDamage(serial, inputAmount);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DamagePacketOld(serial, inputAmount), ns => ns.SendDamage(serial, inputAmount));
}
[Theory, InlineData(10), InlineData(-5), InlineData(1024), InlineData(100000)]
@@ -25,14 +19,10 @@ public class DamagePacketTests
{
var serial = (Serial)0x1024;
- var expected = new DamagePacket(serial, inputAmount).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ProtocolChanges.DamagePacket;
-
- ns.SendDamage(serial, inputAmount);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DamagePacket(serial, inputAmount),
+ ns => ns.SendDamage(serial, inputAmount),
+ ProtocolChanges.DamagePacket
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPacketTests.cs
index 7ade294f6..e9395948d 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPacketTests.cs
@@ -7,18 +7,13 @@ namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
public class EffectPackets
{
- [Theory, InlineData(10, 1000, 10, 5)]
- public void TestSoundEffect(ushort soundID, int x, int y, int z)
+ [Fact]
+ public void TestSoundEffect()
{
- var p = new Point3D(x, y, z);
+ const ushort soundID = 10;
+ var p = new Point3D(1000, 10, 5);
- var expected = new PlaySound(soundID, p).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSoundEffect(soundID, p);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new PlaySound(soundID, p), ns => ns.SendSoundEffect(soundID, p));
}
[Fact]
@@ -94,13 +89,7 @@ public class EffectPackets
[Theory, InlineData(ScreenEffectType.DarkFlash), InlineData(ScreenEffectType.FadeInOut)]
public void TestScreenEffect(ScreenEffectType screenType)
{
- var expected = new ScreenEffect(screenType).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendScreenEffect(screenType);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ScreenEffect(screenType), ns => ns.SendScreenEffect(screenType));
}
[Fact]
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPackets.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPackets.cs
index e18256383..2899569f8 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPackets.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EffectPackets.cs
@@ -76,157 +76,6 @@ public class HuedEffect : Packet
}
}
-public sealed class TargetParticleEffect : ParticleEffect
-{
- public TargetParticleEffect(
- IEntity e, int itemID, int speed, int duration, int hue, int renderMode, int effect,
- int layer, int unknown
- ) : base(
- EffectType.FixedFrom,
- e.Serial,
- Serial.Zero,
- itemID,
- e.Location,
- e.Location,
- speed,
- duration,
- true,
- false,
- hue,
- renderMode,
- effect,
- 1,
- 0,
- e.Serial,
- layer,
- unknown
- )
- {
- }
-}
-
-public sealed class TargetEffect : HuedEffect
-{
- public TargetEffect(IEntity e, int itemID, int speed, int duration, int hue, int renderMode) : base(
- EffectType.FixedFrom,
- e.Serial,
- Serial.Zero,
- itemID,
- e.Location,
- e.Location,
- speed,
- duration,
- true,
- false,
- hue,
- renderMode
- )
- {
- }
-}
-
-public sealed class LocationParticleEffect : ParticleEffect
-{
- public LocationParticleEffect(
- IEntity e, int itemID, int speed, int duration, int hue, int renderMode, int effect,
- int unknown
- ) : base(
- EffectType.FixedXYZ,
- e.Serial,
- Serial.Zero,
- itemID,
- e.Location,
- e.Location,
- speed,
- duration,
- true,
- false,
- hue,
- renderMode,
- effect,
- 1,
- 0,
- e.Serial,
- 255,
- unknown
- )
- {
- }
-}
-
-public sealed class LocationEffect : HuedEffect
-{
- public LocationEffect(IPoint3D p, int itemID, int speed, int duration, int hue, int renderMode) : base(
- EffectType.FixedXYZ,
- Serial.Zero,
- Serial.Zero,
- itemID,
- p,
- p,
- speed,
- duration,
- true,
- false,
- hue,
- renderMode
- )
- {
- }
-}
-
-public sealed class MovingParticleEffect : ParticleEffect
-{
- public MovingParticleEffect(
- IEntity from, IEntity to, int itemID, int speed, int duration, bool fixedDirection,
- bool explodes, int hue, int renderMode, int effect, int explodeEffect, int explodeSound, EffectLayer layer,
- int unknown
- ) : base(
- EffectType.Moving,
- from.Serial,
- to.Serial,
- itemID,
- from.Location,
- to.Location,
- speed,
- duration,
- fixedDirection,
- explodes,
- hue,
- renderMode,
- effect,
- explodeEffect,
- explodeSound,
- Serial.Zero,
- (int)layer,
- unknown
- )
- {
- }
-}
-
-public sealed class MovingEffect : HuedEffect
-{
- public MovingEffect(
- IEntity from, IEntity to, int itemID, int speed, int duration, bool fixedDirection,
- bool explodes, int hue, int renderMode
- ) : base(
- EffectType.Moving,
- from.Serial,
- to.Serial,
- itemID,
- from.Location,
- to.Location,
- speed,
- duration,
- fixedDirection,
- explodes,
- hue,
- renderMode
- )
- {
- }
-}
-
public class ScreenEffect : Packet
{
public ScreenEffect(ScreenEffectType type)
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs
index 8dc535e03..21933bc91 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs
@@ -31,19 +31,16 @@ public class EquipmentPacketTests
}
);
- var expected = new DisplayEquipmentInfo(item, info).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayEquipmentInfo(
- item.Serial,
- info.Number,
- info.Crafter?.RawName,
- info.Unidentified,
- new List(info.Attributes)
+ PacketAssert.Sends(
+ new DisplayEquipmentInfo(item, info),
+ ns => ns.SendDisplayEquipmentInfo(
+ item.Serial,
+ info.Number,
+ info.Crafter?.RawName,
+ info.Unidentified,
+ new List(info.Attributes)
+ )
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -54,12 +51,6 @@ public class EquipmentPacketTests
var item = new Item(World.NewItem) { Parent = m };
- var expected = new EquipUpdate(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendEquipUpdate(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new EquipUpdate(item), ns => ns.SendEquipUpdate(item));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs
index edbe6b668..febac5288 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs
@@ -6,17 +6,13 @@ namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
public class GumpPacketTests
{
- [Theory]
- [InlineData(100, 10)]
- public void TestCloseGump(int typeId, int buttonId)
+ [Fact]
+ public void TestCloseGump()
{
- var expected = new CloseGump(typeId, buttonId).Compile();
+ const int typeId = 100;
+ const int buttonId = 10;
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCloseGump(typeId, buttonId);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new CloseGump(typeId, buttonId), ns => ns.SendCloseGump(typeId, buttonId));
}
[Fact]
@@ -27,13 +23,10 @@ public class GumpPacketTests
const string unknownString = "This is an unknown string";
const string caption = "This is a caption";
- var expected = new DisplaySignGump(gumpSerial, gumpId, unknownString, caption).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplaySignGump(gumpSerial, gumpId, unknownString, caption);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplaySignGump(gumpSerial, gumpId, unknownString, caption),
+ ns => ns.SendDisplaySignGump(gumpSerial, gumpId, unknownString, caption)
+ );
}
[Fact]
@@ -41,13 +34,7 @@ public class GumpPacketTests
{
var gump = new NameChangeDeedGump();
- using var ns = PacketTestUtilities.CreateTestNetState();
-
- var expected = gump.Compile(ns).Compile();
- ns.SendGump(gump);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(ns => gump.Compile(ns), ns => ns.SendGump(gump));
}
[Fact]
@@ -60,13 +47,7 @@ public class GumpPacketTests
var gump = new AdminGump(m, AdminGumpPage.Clients);
- using var ns = PacketTestUtilities.CreateTestNetState();
-
- var expected = gump.Compile(ns).Compile();
- ns.SendGump(gump);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(ns => gump.Compile(ns), ns => ns.SendGump(gump));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs
index 27bad8aaf..f9d5cb948 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs
@@ -1,116 +1,75 @@
+using System;
using Server.Network;
using Xunit;
namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
-public class ItemPacketTests
+public class ItemPacketTests : IDisposable
{
+ private const int TestItemId = 1;
+
+ // TileData is global and holds real client data when client files are loaded, so each test restores it.
+ private readonly ItemData _originalItemData;
+
+ public ItemPacketTests()
+ {
+ _originalItemData = TileData.ItemTable[TestItemId];
+ TileData.ItemTable[TestItemId] = new ItemData("Test Item Data", TileFlag.Generic, 1, 1, 1, 1, 1, 1);
+ }
+
+ public void Dispose() => TileData.ItemTable[TestItemId] = _originalItemData;
+
[Fact]
public void TestWorldItemPacket()
{
var serial = (Serial)0x1024;
- var itemId = 1;
-
- // Move to fixture
- TileData.ItemTable[itemId] = new ItemData(
- "Test Item Data",
- TileFlag.Generic,
- 1,
- 1,
- 1,
- 1,
- 1,
- 1
- );
var item = new Item(serial)
{
- ItemID = itemId,
+ ItemID = TestItemId,
Hue = 0x1024,
Amount = 10,
Location = new Point3D(1000, 100, -10),
Direction = Direction.Left
};
- var expected = new WorldItem(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendWorldItem(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new WorldItem(item), ns => ns.SendWorldItem(item));
}
[Fact]
public void TestWorldItemSAPacket()
{
var serial = (Serial)0x1024;
- ushort itemId = 1;
-
- // Move to fixture
- TileData.ItemTable[itemId] = new ItemData(
- "Test Item Data",
- TileFlag.Generic,
- 1,
- 1,
- 1,
- 1,
- 1,
- 1
- );
var item = new Item(serial)
{
- ItemID = itemId,
+ ItemID = TestItemId,
Hue = 0x1024,
Amount = 10,
Location = new Point3D(1000, 100, -10)
};
- var expected = new WorldItemSA(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ProtocolChanges.StygianAbyss;
- ns.SendWorldItem(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new WorldItemSA(item), ns => ns.SendWorldItem(item), ProtocolChanges.StygianAbyss);
}
[Fact]
public void TestWorldItemHSPacket()
{
var serial = (Serial)0x1024;
- var itemId = 1;
-
- // Move to fixture
- TileData.ItemTable[itemId] = new ItemData(
- "Test Item Data",
- TileFlag.Generic,
- 1,
- 1,
- 1,
- 1,
- 1,
- 1
- );
var item = new Item(serial)
{
- ItemID = itemId,
+ ItemID = TestItemId,
Hue = 0x1024,
Amount = 10,
Location = new Point3D(1000, 100, -10)
};
- var expected = new WorldItemHS(item).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = ProtocolChanges.StygianAbyss | ProtocolChanges.HighSeas;
- ns.SendWorldItem(item);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new WorldItemHS(item),
+ ns => ns.SendWorldItem(item),
+ ProtocolChanges.StygianAbyss | ProtocolChanges.HighSeas
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPackets.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPackets.cs
index 084ba4587..90bd89dfb 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPackets.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPackets.cs
@@ -187,21 +187,3 @@ public sealed class WorldItemHS : Packet
Stream.Write((short)0x00); // ??
}
}
-
-public sealed class OPLInfo : Packet
-{
- /*public OPLInfo( ObjectPropertyList list ) : base( 0xBF )
- {
- EnsureCapacity( 13 );
-
- m_Stream.Write( (short) 0x10 );
- m_Stream.Write( (int) list.Entity.Serial );
- m_Stream.Write( (int) list.Hash );
- }*/
-
- public OPLInfo(Serial serial, int hash) : base(0xDC, 9)
- {
- Stream.Write(serial);
- Stream.Write(hash);
- }
-}
\ No newline at end of file
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs
index c66a61ca7..22ed4c423 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs
@@ -10,13 +10,8 @@ public class LightPacketTests
public void TestGlobalLightLevel()
{
const byte lightLevel = 5;
- var expected = new GlobalLightLevel(lightLevel).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendGlobalLightLevel(lightLevel);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new GlobalLightLevel(lightLevel), ns => ns.SendGlobalLightLevel(lightLevel));
}
[Fact]
@@ -24,12 +19,10 @@ public class LightPacketTests
{
var serial = (Serial)0x1024;
byte lightLevel = 5;
- var expected = new PersonalLightLevel(serial, lightLevel).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPersonalLightLevel(serial, lightLevel);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new PersonalLightLevel(serial, lightLevel),
+ ns => ns.SendPersonalLightLevel(serial, lightLevel)
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs
index a438730e0..d563db252 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs
@@ -9,26 +9,13 @@ public class MapPatchesTests
[Fact]
public void TestMapPatches()
{
- using var ns = PacketTestUtilities.CreateTestNetState();
-
- var expected = new MapPatches().Compile();
-
- ns.SendMapPatches();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MapPatches(), ns => ns.SendMapPatches());
}
[Fact]
public void TestInvalidMapEnable()
{
- var expected = new InvalidMapEnable().Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendInvalidMap();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new InvalidMapEnable(), ns => ns.SendInvalidMap());
}
[Theory]
@@ -37,12 +24,7 @@ public class MapPatchesTests
public void TestMapChange(string mapName)
{
var map = Map.Parse(mapName);
- var expected = new MapChange(map).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMapChange(map);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MapChange(map), ns => ns.SendMapChange(map));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs
index 3da55c89e..c87355ac8 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs
@@ -42,13 +42,7 @@ public class MenuPacketTests
]
);
- var expected = new DisplayItemListMenu(menu).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayItemListMenu(menu);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DisplayItemListMenu(menu), ns => ns.SendDisplayItemListMenu(menu));
}
[Fact]
@@ -63,12 +57,7 @@ public class MenuPacketTests
]
);
- var expected = new DisplayQuestionMenu(menu).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayQuestionMenu(menu);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DisplayQuestionMenu(menu), ns => ns.SendDisplayQuestionMenu(menu));
}
[Theory]
@@ -84,18 +73,12 @@ public class MenuPacketTests
var item = new ContextMenuItem(World.NewItem, newPacket);
var menu = ContextMenuSystem.CreateContextMenu(m, item);
- var packet = newHaven && newPacket ? (Packet)new DisplayContextMenu(menu) : new DisplayContextMenuOld(menu);
- var expected = packet.Compile();
+ var expected = newHaven && newPacket ? (Packet)new DisplayContextMenu(menu) : new DisplayContextMenuOld(menu);
- using var ns = PacketTestUtilities.CreateTestNetState();
- if (newHaven)
- {
- ns.ProtocolChanges |= ProtocolChanges.NewHaven;
- }
-
- ns.SendDisplayContextMenu(menu);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ expected,
+ ns => ns.SendDisplayContextMenu(menu),
+ newHaven ? ProtocolChanges.NewHaven : ProtocolChanges.None
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MessagePacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MessagePacketTests.cs
index b5eb423f1..c1911db57 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MessagePacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MessagePacketTests.cs
@@ -19,31 +19,10 @@ public class MessageTests
var name = "Stuff";
var args = "Arguments";
- var expected = new MessageLocalized(
- serial,
- graphic,
- messageType,
- hue,
- font,
- number,
- name,
- args
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessageLocalized(
- serial,
- graphic,
- messageType,
- hue,
- font,
- number,
- name,
- args
+ PacketAssert.Sends(
+ new MessageLocalized(serial, graphic, messageType, hue, font, number, name, args),
+ ns => ns.SendMessageLocalized(serial, graphic, messageType, hue, font, number, name, args)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -58,15 +37,10 @@ public class MessageTests
var name = "Stuff";
var argValue = 42;
- var expected = new MessageLocalized(
- serial, graphic, messageType, hue, font, number, name, $"value: {argValue}"
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessageLocalized(serial, graphic, messageType, hue, font, number, name, $"value: {argValue}");
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new MessageLocalized(serial, graphic, messageType, hue, font, number, name, $"value: {argValue}"),
+ ns => ns.SendMessageLocalized(serial, graphic, messageType, hue, font, number, name, $"value: {argValue}")
+ );
}
[Fact]
@@ -83,35 +57,12 @@ public class MessageTests
var affixType = AffixType.System;
var affix = "Affix";
- var expected = new MessageLocalizedAffix(
- serial,
- graphic,
- messageType,
- hue,
- font,
- number,
- name,
- affixType,
- affix,
- args
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessageLocalizedAffix(
- serial,
- graphic,
- messageType,
- hue,
- font,
- number,
- name,
- affixType,
- affix,
- args
+ PacketAssert.Sends(
+ new MessageLocalizedAffix(serial, graphic, messageType, hue, font, number, name, affixType, affix, args),
+ ns => ns.SendMessageLocalizedAffix(
+ serial, graphic, messageType, hue, font, number, name, affixType, affix, args
+ )
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -128,18 +79,14 @@ public class MessageTests
var affix = "Affix";
var argValue = 42;
- var expected = new MessageLocalizedAffix(
- serial, graphic, messageType, hue, font, number, name, affixType, affix, $"value: {argValue}"
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessageLocalizedAffix(
- serial, graphic, messageType, hue, font, number, name, affixType, affix,
- $"value: {argValue}"
+ PacketAssert.Sends(
+ new MessageLocalizedAffix(
+ serial, graphic, messageType, hue, font, number, name, affixType, affix, $"value: {argValue}"
+ ),
+ ns => ns.SendMessageLocalizedAffix(
+ serial, graphic, messageType, hue, font, number, name, affixType, affix, $"value: {argValue}"
+ )
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -154,17 +101,10 @@ public class MessageTests
var name = "Stuff";
var who = "Alice";
- var expected = new UnicodeMessage(
- serial, graphic, messageType, hue, font, lang, name, $"hello, {who}"
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessage(
- serial, graphic, messageType, hue, font, false, lang, name, $"hello, {who}"
+ PacketAssert.Sends(
+ new UnicodeMessage(serial, graphic, messageType, hue, font, lang, name, $"hello, {who}"),
+ ns => ns.SendMessage(serial, graphic, messageType, hue, font, false, lang, name, $"hello, {who}")
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -178,31 +118,10 @@ public class MessageTests
var name = "Stuff";
var text = "Some Text";
- var expected = new AsciiMessage(
- serial,
- graphic,
- messageType,
- hue,
- font,
- name,
- text
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessage(
- serial,
- graphic,
- messageType,
- hue,
- font,
- true,
- null,
- name,
- text
+ PacketAssert.Sends(
+ new AsciiMessage(serial, graphic, messageType, hue, font, name, text),
+ ns => ns.SendMessage(serial, graphic, messageType, hue, font, true, null, name, text)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -217,32 +136,10 @@ public class MessageTests
var name = "Stuff";
var text = "Some Text";
- var expected = new UnicodeMessage(
- serial,
- graphic,
- messageType,
- hue,
- font,
- lang,
- name,
- text
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMessage(
- serial,
- graphic,
- messageType,
- hue,
- font,
- false,
- lang,
- name,
- text
+ PacketAssert.Sends(
+ new UnicodeMessage(serial, graphic, messageType, hue, font, lang, name, text),
+ ns => ns.SendMessage(serial, graphic, messageType, hue, font, false, lang, name, text)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Fact]
@@ -251,13 +148,7 @@ public class MessageTests
var serial = (Serial)0x1024;
var serial2 = (Serial)0x2;
- var expected = new FollowMessage(serial, serial2).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendFollowMessage(serial, serial2);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new FollowMessage(serial, serial2), ns => ns.SendFollowMessage(serial, serial2));
}
[Fact]
@@ -266,33 +157,18 @@ public class MessageTests
var s = (Serial)0x100;
var text = "This is some testing text";
- var expected = new ObjectHelpResponse(s, text).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendHelpResponse(s, text);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ObjectHelpResponse(s, text), ns => ns.SendHelpResponse(s, text));
}
- internal class TestPrompt : Prompt
+ [Fact]
+ public void TestUnicodePrompt()
{
+ var prompt = new TestPrompt();
+
+ PacketAssert.Sends(new UnicodePrompt(prompt), ns => ns.SendPrompt(prompt));
}
- [Collection("Sequential Server Tests")]
- public class UnicodePromptTests
+ private class TestPrompt : Prompt
{
- [Fact]
- public void TestUnicodePrompt()
- {
- var prompt = new TestPrompt();
- var expected = new UnicodePrompt(prompt).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPrompt(prompt);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
- }
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs
index d7d3beca2..3d839899b 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs
@@ -6,19 +6,38 @@ namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
public class MobilePacketTests
{
+ private static Mobile CreateMobileWithStats(uint serial = 0x1, string name = null)
+ {
+ var m = new Mobile((Serial)serial) { Name = name };
+ m.DefaultMobileInit();
+ m.Str = 50;
+ m.Hits = 100;
+ m.Int = 75;
+ m.Mana = 100;
+ m.Dex = 25;
+ m.Stam = 100;
+
+ return m;
+ }
+
+ private sealed class TestPoison : Poison
+ {
+ public TestPoison(int level) : base(level) => Level = level;
+
+ public override string Name => $"Test Poison {Level}";
+ public override int Level { get; }
+ public override PoisonFamily Family => PoisonFamily.Standard;
+
+ public override Timer ConstructTimer(Mobile m) => null;
+ }
+
[Fact]
public void TestDeathAnimation()
{
var killed = (Serial)0x1;
var corpse = (Serial)0x1000;
- var expected = new DeathAnimation(killed, corpse).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDeathAnimation(killed, corpse);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DeathAnimation(killed, corpse), ns => ns.SendDeathAnimation(killed, corpse));
}
[Fact]
@@ -27,13 +46,7 @@ public class MobilePacketTests
var petSerial = (Serial)0x1;
const bool bonded = true;
- var expected = new BondedStatus(petSerial, bonded).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendBondedStatus(petSerial, bonded);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new BondedStatus(petSerial, bonded), ns => ns.SendBondedStatus(petSerial, bonded));
}
[Theory]
@@ -46,14 +59,11 @@ public class MobilePacketTests
var noto = 10;
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = protocolChanges;
- var expected = new MobileMoving(m, noto, ns.StygianAbyss).Compile();
-
- ns.SendMobileMoving(m, noto);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ ns => new MobileMoving(m, noto, ns.StygianAbyss),
+ ns => ns.SendMobileMoving(m, noto),
+ protocolChanges
+ );
}
[Theory]
@@ -65,13 +75,7 @@ public class MobilePacketTests
var m = new Mobile((Serial)0x1) { Name = name };
m.DefaultMobileInit();
- var expected = new MobileName(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileName(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileName(m), ns => ns.SendMobileName(m));
}
[Theory]
@@ -81,29 +85,10 @@ public class MobilePacketTests
{
var mobile = (Serial)0x1;
- var expected = new MobileAnimation(
- mobile,
- action,
- frameCount,
- repeatCount,
- !reverse,
- repeat,
- delay
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileAnimation(
- mobile,
- action,
- frameCount,
- repeatCount,
- !reverse,
- repeat,
- delay
+ PacketAssert.Sends(
+ new MobileAnimation(mobile, action, frameCount, repeatCount, !reverse, repeat, delay),
+ ns => ns.SendMobileAnimation(mobile, action, frameCount, repeatCount, !reverse, repeat, delay)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Theory]
@@ -113,43 +98,24 @@ public class MobilePacketTests
{
var mobile = (Serial)0x1;
- var expected = new NewMobileAnimation(
- mobile,
- action,
- frameCount,
- delay
- ).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendNewMobileAnimation(
- mobile,
- action,
- frameCount,
- delay
+ PacketAssert.Sends(
+ new NewMobileAnimation(mobile, action, frameCount, delay),
+ ns => ns.SendNewMobileAnimation(mobile, action, frameCount, delay)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
+ // Level -1 means not poisoned. Constructed directly: the poison registry is filled by UOContent.
[Theory]
- [InlineData("None")]
- [InlineData("Lesser")]
- [InlineData("Lethal")]
- public void TestHealthbarPoison(string pName)
+ [InlineData(-1)]
+ [InlineData(0)]
+ [InlineData(4)]
+ public void TestHealthbarPoison(int level)
{
- var p = Poison.GetPoison(pName);
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
- m.Poison = p;
+ m.Poison = level < 0 ? null : new TestPoison(level);
- var expected = new HealthbarPoison(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileHealthbar(m, Healthbar.Poison);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new HealthbarPoison(m), ns => ns.SendMobileHealthbar(m, Healthbar.Poison));
}
[Theory]
@@ -164,13 +130,7 @@ public class MobilePacketTests
m.Blessed = isBlessed;
m.YellowHealthbar = isYellowHealth;
- var expected = new HealthbarYellow(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileHealthbar(m, Healthbar.Yellow);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new HealthbarYellow(m), ns => ns.SendMobileHealthbar(m, Healthbar.Yellow));
}
[Theory]
@@ -178,22 +138,9 @@ public class MobilePacketTests
[InlineData(false)]
public void TestMobileStatusCompact(bool canBeRenamed)
{
- var m = new Mobile((Serial)0x1) { Name = "Random Mobile 1" };
- m.DefaultMobileInit();
- m.Str = 50;
- m.Hits = 100;
- m.Int = 75;
- m.Mana = 100;
- m.Dex = 25;
- m.Stam = 100;
+ var m = CreateMobileWithStats(name: "Random Mobile 1");
- var expected = new MobileStatusCompact(canBeRenamed, m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileStatusCompact(m, canBeRenamed);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileStatusCompact(canBeRenamed, m), ns => ns.SendMobileStatusCompact(m, canBeRenamed));
}
[Theory]
@@ -201,32 +148,14 @@ public class MobilePacketTests
[InlineData(ProtocolChanges.Version502b)]
public void TestMobileStatus(ProtocolChanges changes)
{
- var beholder = new Mobile((Serial)0x1) { Name = "Random Mobile 1" };
- beholder.DefaultMobileInit();
- beholder.Str = 50;
- beholder.Hits = 100;
- beholder.Int = 75;
- beholder.Mana = 100;
- beholder.Dex = 25;
- beholder.Stam = 100;
+ var beholder = CreateMobileWithStats(0x1, "Random Mobile 1");
+ var beheld = CreateMobileWithStats(0x2, "Random Mobile 2");
- var beheld = new Mobile((Serial)0x2) { Name = "Random Mobile 2" };
- beheld.DefaultMobileInit();
- beheld.Str = 50;
- beheld.Hits = 100;
- beheld.Int = 75;
- beheld.Mana = 100;
- beheld.Dex = 25;
- beheld.Stam = 100;
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = changes;
-
- var expected = new MobileStatus(beholder, beheld, ns).Compile();
- ns.SendMobileStatus(beholder, beheld);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ ns => new MobileStatus(beholder, beheld, ns),
+ ns => ns.SendMobileStatus(beholder, beheld),
+ changes
+ );
}
[Theory]
@@ -240,33 +169,32 @@ public class MobilePacketTests
int mobileStatusVersion
)
{
- var expansionInfo = ExpansionInfo.CoreExpansion;
var oldExpansion = Core.Expansion;
+ var expansionInfo = ExpansionInfo.GetInfo(expansion);
var oldVersion = expansionInfo.MobileStatusVersion;
- Core.Expansion = expansion;
- ExpansionInfo.CoreExpansion.MobileStatusVersion = mobileStatusVersion;
- var m = new Mobile((Serial)0x1) { Name = "Random Mobile 1" };
- m.DefaultMobileInit();
- m.Str = 50;
- m.Hits = 100;
- m.Int = 75;
- m.Mana = 100;
- m.Dex = 25;
- m.Stam = 100;
+ try
+ {
+ Core.Expansion = expansion;
+ expansionInfo.MobileStatusVersion = mobileStatusVersion;
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = changes;
- ns.Version = new ClientVersion(version);
- ns.Flags = clientFlags;
+ var m = CreateMobileWithStats(name: "Random Mobile 1");
- var expected = new MobileStatusExtended(m, ns).Compile();
- ns.SendMobileStatus(m, m);
+ using var ns = PacketTestUtilities.CreateTestNetState();
+ ns.ProtocolChanges = changes;
+ ns.Version = new ClientVersion(version);
+ ns.Flags = clientFlags;
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
- Core.Expansion = oldExpansion;
- expansionInfo.MobileStatusVersion = oldVersion;
+ var expected = new MobileStatusExtended(m, ns).Compile();
+ ns.SendMobileStatus(m, m);
+
+ AssertThat.Equal(ns.SendBuffer.GetReadSpan(), expected);
+ }
+ finally
+ {
+ Core.Expansion = oldExpansion;
+ expansionInfo.MobileStatusVersion = oldVersion;
+ }
}
[Theory]
@@ -278,14 +206,7 @@ public class MobilePacketTests
m.DefaultMobileInit();
m.SolidHueOverride = solidHueOverride;
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = changes;
-
- var expected = new MobileUpdate(m, ns.StygianAbyss).Compile();
- ns.SendMobileUpdate(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(ns => new MobileUpdate(m, ns.StygianAbyss), ns => ns.SendMobileUpdate(m), changes);
}
[Theory]
@@ -333,170 +254,82 @@ public class MobilePacketTests
beheld.FacialHairItemID = facialHairItemId;
beheld.FacialHairHue = facialHairHue;
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = changes;
-
- var expected = new MobileIncoming(ns, beholder, beheld).Compile();
- ns.SendMobileIncoming(beholder, beheld);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ ns => new MobileIncoming(ns, beholder, beheld),
+ ns => ns.SendMobileIncoming(beholder, beheld),
+ changes
+ );
}
[Fact]
public void TestMobileHits()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Str = 100;
- m.Hits = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileHits(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileHits(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileHits(m), ns => ns.SendMobileHits(m));
}
[Fact]
public void TestMobileHitsN()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Str = 100;
- m.Hits = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileHitsN(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileHits(m, true);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileHitsN(m), ns => ns.SendMobileHits(m, true));
}
[Fact]
public void TestMobileMana()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Int = 75;
- m.Mana = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileMana(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileMana(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileMana(m), ns => ns.SendMobileMana(m));
}
[Fact]
public void TestMobileManaN()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Int = 75;
- m.Mana = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileManaN(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileMana(m, true);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileManaN(m), ns => ns.SendMobileMana(m, true));
}
[Fact]
public void TestMobileStam()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Dex = 75;
- m.Stam = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileStam(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileStam(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileStam(m), ns => ns.SendMobileStam(m));
}
[Fact]
public void TestMobileStamN()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Dex = 75;
- m.Stam = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileStamN(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileStam(m, true);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileStamN(m), ns => ns.SendMobileStam(m, true));
}
[Fact]
public void TestMobileAttributes()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Str = 50;
- m.Hits = 100;
- m.Int = 75;
- m.Mana = 100;
- m.Dex = 25;
- m.Stam = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileAttributes(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileAttributes(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileAttributes(m), ns => ns.SendMobileAttributes(m));
}
[Fact]
public void TestMobileAttributesN()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
- m.Str = 50;
- m.Hits = 100;
- m.Int = 75;
- m.Mana = 100;
- m.Dex = 25;
- m.Stam = 100;
+ var m = CreateMobileWithStats();
- var expected = new MobileAttributesN(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMobileAttributes(m, true);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MobileAttributesN(m), ns => ns.SendMobileAttributes(m, true));
}
[Fact]
public void TestRemoveEntity()
{
var e = (Serial)0x1000;
- var expected = new RemoveEntity(e).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendRemoveEntity(e);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new RemoveEntity(e), ns => ns.SendRemoveEntity(e));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs
index 330eaf0b5..5f8a27206 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs
@@ -12,26 +12,18 @@ public class MovementPacketTests
[InlineData(2)]
public void TestSpeedControl(byte speedControl)
{
- var expected = new SpeedControl(speedControl).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSpeedControl((SpeedControlSetting)speedControl);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new SpeedControl(speedControl),
+ ns => ns.SendSpeedControl((SpeedControlSetting)speedControl)
+ );
}
[Fact]
public void TestMovePlayer()
{
const Direction d = Direction.Left;
- var expected = new MovePlayer(d).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMovePlayer(d);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MovePlayer(d), ns => ns.SendMovePlayer(d));
}
[Fact]
@@ -42,13 +34,7 @@ public class MovementPacketTests
const byte seq = 100;
- var expected = new MovementRej(seq, m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMovementRej(seq, m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MovementRej(seq, m), ns => ns.SendMovementRej(seq, m));
}
[Fact]
@@ -59,12 +45,6 @@ public class MovementPacketTests
const byte seq = 100;
- var expected = MovementAck.Instantiate(seq, m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMovementAck(seq, m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(MovementAck.Instantiate(seq, m), ns => ns.SendMovementAck(seq, m));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs
index 6d6e5daa3..8bfe17a9e 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs
@@ -8,23 +8,16 @@ namespace Server.Tests.Network;
[Collection("Sequential Server Tests")]
public class PlayerPacketTests
{
- [Theory]
- [InlineData(StatLockType.Down, StatLockType.Up, StatLockType.Locked)]
- public void TestStatLockInfo(StatLockType str, StatLockType intel, StatLockType dex)
+ [Fact]
+ public void TestStatLockInfo()
{
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
- m.StrLock = str;
- m.IntLock = intel;
- m.DexLock = dex;
+ m.StrLock = StatLockType.Down;
+ m.IntLock = StatLockType.Up;
+ m.DexLock = StatLockType.Locked;
- var expected = new StatLockInfo(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendStatLockInfo(m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new StatLockInfo(m), ns => ns.SendStatLockInfo(m));
}
[Theory]
@@ -33,25 +26,13 @@ public class PlayerPacketTests
[InlineData(200)]
public void TestChangeUpdateRange(int range)
{
- var expected = new ChangeUpdateRange(range).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendChangeUpdateRange((byte)range);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ChangeUpdateRange(range), ns => ns.SendChangeUpdateRange((byte)range));
}
[Fact]
public void TestDeathStatus()
{
- var expected = new DeathStatus(false).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDeathStatus();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new DeathStatus(false), ns => ns.SendDeathStatus());
}
[Theory]
@@ -59,13 +40,10 @@ public class PlayerPacketTests
[InlineData(0x1000u, null, null, null)]
public void TestDisplayProfile(uint serial, string header, string body, string footer)
{
- var expected = new DisplayProfile((Serial)serial, header, body, footer).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayProfile((Serial)serial, header, body, footer);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplayProfile((Serial)serial, header, body, footer),
+ ns => ns.SendDisplayProfile((Serial)serial, header, body, footer)
+ );
}
[Theory]
@@ -73,55 +51,31 @@ public class PlayerPacketTests
[InlineData(LRReason.TryToSteal)]
public void TestLiftRej(LRReason reason)
{
- var expected = new LiftRej(reason).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendLiftReject(reason);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new LiftRej(reason), ns => ns.SendLiftReject(reason));
}
[Fact]
public void TestLogoutAck()
{
- var expected = new LogoutAck().Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendLogoutAck();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new LogoutAck(), ns => ns.SendLogoutAck());
}
[Theory]
[InlineData(1, 2, 3)]
[InlineData(4, 5, 6)]
- [InlineData(0x1234, 0x5678, 0x9ABC)]
- public void TestWeather(int type, int density, int temp)
+ [InlineData(0xFF, 0x46, 0xEC)]
+ public void TestWeather(byte type, byte density, byte temp)
{
- var expected = new Weather(type, density, temp).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendWeather((byte)type, (byte)density, (byte)temp);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new Weather(type, density, temp), ns => ns.SendWeather(type, density, temp));
}
- [Theory]
- [InlineData(100, 1000, 1, 0)]
- public void TestServerChange(int x, int y, int z, int mapID)
+ [Fact]
+ public void TestServerChange()
{
- var p = new Point3D(x, y, z);
- var map = Map.Maps[mapID];
- var expected = new ServerChange(p, map).Compile();
+ var p = new Point3D(100, 1000, 1);
+ var map = Map.Maps[0];
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendServerChange(p, map);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ServerChange(p, map), ns => ns.SendServerChange(p, map));
}
[Theory]
@@ -130,13 +84,7 @@ public class PlayerPacketTests
[InlineData(255)]
public void TestSequence(byte num)
{
- var expected = new Sequence(num).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSequence(num);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new Sequence(num), ns => ns.SendSequence(num));
}
[Theory]
@@ -146,37 +94,22 @@ public class PlayerPacketTests
[InlineData("https://www.modernuo.com")]
public void TestLaunchBrowser(string uri)
{
- var expected = new LaunchBrowser(uri).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendLaunchBrowser(uri);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new LaunchBrowser(uri), ns => ns.SendLaunchBrowser(uri));
}
- [Theory]
- [InlineData(0x1000u, 1000, 100, 10, 0x2000u, 1125, 125, 5, 0x384, 1024, 25)]
- public void TestDragEffect(
- uint srcSerial, int srcX, int srcY, int srcZ,
- uint trgSerial, int trgX, int trgY, int trgZ,
- int itemId, int hue, int amount
- )
+ [Fact]
+ public void TestDragEffect()
{
- var src = new Entity((Serial)srcSerial, new Point3D(srcX, srcY, srcZ), null);
- var targ = new Entity((Serial)trgSerial, new Point3D(trgX, trgY, trgZ), null);
+ var src = new Entity((Serial)0x1000, new Point3D(1000, 100, 10), null);
+ var targ = new Entity((Serial)0x2000, new Point3D(1125, 125, 5), null);
+ const int itemId = 0x384;
+ const int hue = 1024;
+ const int amount = 25;
- var expected = new DragEffect(src, targ, itemId, hue, amount).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDragEffect(
- src.Serial, src.Location,
- targ.Serial, targ.Location,
- itemId, hue, amount
+ PacketAssert.Sends(
+ new DragEffect(src, targ, itemId, hue, amount),
+ ns => ns.SendDragEffect(src.Serial, src.Location, targ.Serial, targ.Location, itemId, hue, amount)
);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
}
[Theory]
@@ -184,13 +117,7 @@ public class PlayerPacketTests
[InlineData(2, true)]
public void TestSeasonChange(int season, bool playSound)
{
- var expected = new SeasonChange(season, playSound).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSeasonChange((byte)season, playSound);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new SeasonChange(season, playSound), ns => ns.SendSeasonChange((byte)season, playSound));
}
[Theory]
@@ -199,13 +126,10 @@ public class PlayerPacketTests
[InlineData(0x1024u, "Test Title", true, false)]
public void TestDisplayPaperdoll(uint m, string title, bool warmode, bool canLift)
{
- var expected = new DisplayPaperdoll((Serial)m, title, warmode, canLift).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayPaperdoll((Serial)m, title, warmode, canLift);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplayPaperdoll((Serial)m, title, warmode, canLift),
+ ns => ns.SendDisplayPaperdoll((Serial)m, title, warmode, canLift)
+ );
}
[Theory]
@@ -214,13 +138,7 @@ public class PlayerPacketTests
[InlineData(MusicName.ValoriaShips)]
public void TestPlayMusic(MusicName music)
{
- var expected = new PlayMusic(music).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPlayMusic(music);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new PlayMusic(music), ns => ns.SendPlayMusic(music));
}
[Theory]
@@ -228,41 +146,23 @@ public class PlayerPacketTests
[InlineData(100, 10, "Some more text")]
public void TestScrollMessage(int type, int tip, string text)
{
- var expected = new ScrollMessage(type, tip, text).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendScrollMessage(type, tip ,text);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new ScrollMessage(type, tip, text), ns => ns.SendScrollMessage(type, tip, text));
}
- [Theory]
- [InlineData(14, 10, 05)]
- public void TestCurrentTime(int hour, int minute, int second)
+ [Fact]
+ public void TestCurrentTime()
{
- var date = new DateTime(2020, 1, 1, hour, minute, second);
- var expected = new CurrentTime(date).Compile();
+ var date = new DateTime(2020, 1, 1, 14, 10, 5);
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCurrentTime(date);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new CurrentTime(date), ns => ns.SendCurrentTime(date));
}
- [Theory]
- [InlineData(1000, 10, 1)]
- public void TestPathfindMessage(int x, int y, int z)
+ [Fact]
+ public void TestPathfindMessage()
{
- var p = new Point3D(x, y, z);
- var expected = new PathfindMessage(p).Compile();
+ var p = new Point3D(1000, 10, 1);
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPathfindMessage(p);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new PathfindMessage(p), ns => ns.SendPathfindMessage(p));
}
[Theory]
@@ -271,27 +171,17 @@ public class PlayerPacketTests
[InlineData(100)]
public void TestPingAck(byte ping)
{
- var expected = new PingAck(ping).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendPingAck(ping);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new PingAck(ping), ns => ns.SendPingAck(ping));
}
- [Theory]
- [InlineData(0xFF01)]
- public void TestDisplayHuePicker(int itemID)
+ [Fact]
+ public void TestDisplayHuePicker()
{
- var huePicker = new HuePicker(itemID);
+ var huePicker = new HuePicker(0xFF01);
- var expected = new DisplayHuePicker(huePicker).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplayHuePicker(huePicker.Serial, huePicker.ItemID);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplayHuePicker(huePicker),
+ ns => ns.SendDisplayHuePicker(huePicker.Serial, huePicker.ItemID)
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs
index 6c10b63e9..ca2934b0c 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs
@@ -18,13 +18,10 @@ public class SecureTradePacketTests
var firstCont = new Container(World.NewItem);
var secondCont = new Container(World.NewItem);
- var expected = new DisplaySecureTrade(m, firstCont, secondCont, name).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendDisplaySecureTrade(m, firstCont, secondCont, name);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new DisplaySecureTrade(m, firstCont, secondCont, name),
+ ns => ns.SendDisplaySecureTrade(m, firstCont, secondCont, name)
+ );
}
[Fact]
@@ -32,13 +29,7 @@ public class SecureTradePacketTests
{
var cont = new Container(World.NewItem);
- var expected = new CloseSecureTrade(cont).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCloseSecureTrade(cont);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new CloseSecureTrade(cont), ns => ns.SendCloseSecureTrade(cont));
}
[Theory, InlineData(true, false), InlineData(false, true)]
@@ -50,13 +41,11 @@ public class SecureTradePacketTests
var secondCont = new Container(World.NewItem);
var cont = first ? firstCont : secondCont;
- var expected = new UpdateSecureTrade(cont, first, second).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendUpdateSecureTrade(cont, first, second);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new UpdateSecureTrade(cont, first, second),
+ ns => ns.SendUpdateSecureTrade(cont, first, second)
+ );
}
[Theory]
@@ -65,13 +54,11 @@ public class SecureTradePacketTests
public void TestUpdateGoldSecureTrade(int gold, int plat, TradeFlag flag)
{
var cont = new Container(World.NewItem);
- var expected = new UpdateSecureTrade(cont, flag, gold, plat).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendUpdateSecureTrade(cont, flag, gold, plat);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new UpdateSecureTrade(cont, flag, gold, plat),
+ ns => ns.SendUpdateSecureTrade(cont, flag, gold, plat)
+ );
}
[Fact]
@@ -83,13 +70,7 @@ public class SecureTradePacketTests
var cont = new Container(World.NewItem);
var itemInCont = new Item(World.NewItem) { Parent = cont };
- var expected = new SecureTradeEquip(itemInCont, m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendSecureTradeEquip(itemInCont, m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new SecureTradeEquip(itemInCont, m), ns => ns.SendSecureTradeEquip(itemInCont, m));
}
[Fact]
@@ -101,14 +82,10 @@ public class SecureTradePacketTests
var cont = new Container(World.NewItem);
var itemInCont = new Item(World.NewItem) { Parent = cont };
- var expected = new SecureTradeEquip6017(itemInCont, m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.ContainerGridLines;
-
- ns.SendSecureTradeEquip(itemInCont, m);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new SecureTradeEquip6017(itemInCont, m),
+ ns => ns.SendSecureTradeEquip(itemInCont, m),
+ ProtocolChanges.ContainerGridLines
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/TargetPacketsTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/TargetPacketsTests.cs
index c2d4d29fc..4cc8112b9 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/TargetPacketsTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/TargetPacketsTests.cs
@@ -38,14 +38,7 @@ public class TargetPacketsTests
var p = new Point3D(1000, 100, 10);
MultiTarget t = new TestMultiTarget(multiID, p);
- var expected = new MultiTargetReqHS(t).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges |= ProtocolChanges.HighSeas;
- ns.SendMultiTargetReq(t);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MultiTargetReqHS(t), ns => ns.SendMultiTargetReq(t), ProtocolChanges.HighSeas);
}
[Fact]
@@ -55,36 +48,20 @@ public class TargetPacketsTests
var p = new Point3D(1000, 100, 10);
MultiTarget t = new TestMultiTarget(multiID, p);
- var expected = new MultiTargetReq(t).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendMultiTargetReq(t);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new MultiTargetReq(t), ns => ns.SendMultiTargetReq(t));
}
[Fact]
public void TestCancelTarget()
{
- var expected = new CancelTarget().Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendCancelTarget();
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new CancelTarget(), ns => ns.SendCancelTarget());
}
[Fact]
public void TestTargetReq()
{
var t = new TestTarget(10, true, TargetFlags.Beneficial);
- var expected = new TargetReq(t).Compile();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendTargetReq(t);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new TargetReq(t), ns => ns.SendTargetReq(t));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs
index 6262cc243..10f498a20 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs
@@ -22,15 +22,11 @@ public class VendorBuyPacketTests
new("Third Item", cont.Serial, World.NewItem, 30, 10, 0x0F, 0)
};
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = protocolChanges;
-
- var expected = new VendorBuyContent(buyStates, ns.ContainerGridLines).Compile();
-
- ns.SendVendorBuyContent(buyStates);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ ns => new VendorBuyContent(buyStates, ns.ContainerGridLines),
+ ns => ns.SendVendorBuyContent(buyStates),
+ protocolChanges
+ );
}
[Theory]
@@ -41,15 +37,11 @@ public class VendorBuyPacketTests
var vendor = new Mobile((Serial)0x1);
vendor.DefaultMobileInit();
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.ProtocolChanges = protocolChanges;
-
- var expected = new DisplayBuyList(vendor.Serial, ns.HighSeas).Compile();
-
- ns.SendDisplayBuyList(vendor.Serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ ns => new DisplayBuyList(vendor.Serial, ns.HighSeas),
+ ns => ns.SendDisplayBuyList(vendor.Serial),
+ protocolChanges
+ );
}
[Fact]
@@ -67,13 +59,7 @@ public class VendorBuyPacketTests
new("Third Item", cont.Serial, World.NewItem, 30, 10, 0x0F, 0)
};
- var expected = new VendorBuyList(vendor, buyStates).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendVendorBuyList(vendor, buyStates);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new VendorBuyList(vendor, buyStates), ns => ns.SendVendorBuyList(vendor, buyStates));
}
[Fact]
@@ -82,12 +68,6 @@ public class VendorBuyPacketTests
var vendor = new Mobile((Serial)0x1);
vendor.DefaultMobileInit();
- var expected = new EndVendorBuy(vendor.Serial).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendEndVendorBuy(vendor.Serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new EndVendorBuy(vendor.Serial), ns => ns.SendEndVendorBuy(vendor.Serial));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs
index 154223349..0b911e6bb 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs
@@ -25,13 +25,10 @@ public class VendorSellPacketTests
new(item3, 1, "Item 3")
};
- var expected = new VendorSellList(vendor, sellStates.ToList()).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendVendorSellList(vendor.Serial, sellStates);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new VendorSellList(vendor, sellStates.ToList()),
+ ns => ns.SendVendorSellList(vendor.Serial, sellStates)
+ );
}
[Fact]
@@ -40,12 +37,6 @@ public class VendorSellPacketTests
var vendor = new Mobile((Serial)0x1024u);
vendor.DefaultMobileInit();
- var expected = new EndVendorBuy(vendor.Serial).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendEndVendorSell(vendor.Serial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new EndVendorSell(vendor), ns => ns.SendEndVendorSell(vendor.Serial));
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs
index 3a105d1a4..2d70896f4 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs
@@ -15,13 +15,10 @@ public class VirtualHairPacketTests
m.HairHue = 0x1000;
m.HairItemID = 0x2000;
- var expected = new HairEquipUpdate(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendHairEquipUpdatePacket(m, (uint)m.HairSerial, m.HairItemID, m.HairHue, Layer.Hair);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(
+ new HairEquipUpdate(m),
+ ns => ns.SendHairEquipUpdatePacket(m, (uint)m.HairSerial, m.HairItemID, m.HairHue, Layer.Hair)
+ );
}
[Fact]
@@ -30,13 +27,7 @@ public class VirtualHairPacketTests
var m = new Mobile((Serial)0x1024u);
m.DefaultMobileInit();
- var expected = new RemoveHair(m).Compile();
-
- using var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendRemoveHairPacket((uint)m.HairSerial);
-
- var result = ns.SendBuffer.GetReadSpan();
- AssertThat.Equal(result, expected);
+ PacketAssert.Sends(new RemoveHair(m), ns => ns.SendRemoveHairPacket((uint)m.HairSerial));
}
[Fact]
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs
index 135a6e698..ac7ebbf59 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs
@@ -16,22 +16,6 @@ public sealed class HairEquipUpdate : Packet
}
}
-public sealed class FacialHairEquipUpdate : Packet
-{
- public FacialHairEquipUpdate(Mobile parent)
- : base(0x2E, 15)
- {
- var hue = parent.SolidHueOverride >= 0 ? parent.SolidHueOverride : parent.FacialHairHue;
-
- Stream.Write(parent.FacialHairSerial);
- Stream.Write((short)parent.FacialHairItemID);
- Stream.Write((byte)0);
- Stream.Write((byte)Layer.FacialHair);
- Stream.Write(parent.Serial);
- Stream.Write((short)hue);
- }
-}
-
public sealed class RemoveHair : Packet
{
public RemoveHair(Mobile parent)
@@ -40,12 +24,3 @@ public sealed class RemoveHair : Packet
Stream.Write(parent.HairSerial);
}
}
-
-public sealed class RemoveFacialHair : Packet
-{
- public RemoveFacialHair(Mobile parent)
- : base(0x1D, 5)
- {
- Stream.Write(parent.FacialHairSerial);
- }
-}
diff --git a/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListHashTests.cs b/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListHashTests.cs
index 19a8704b7..6dbef52b1 100644
--- a/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListHashTests.cs
+++ b/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListHashTests.cs
@@ -102,36 +102,31 @@ public class ObjectPropertyListHashTests
// 6- and 8-byte blocks take xxHash3's short-input paths. They still avalanche across all 26
// kept bits, so a counter ticking down never repeats the revision it just had.
[Fact]
- public void ShortNumericArguments_ConsecutiveValuesDiffer()
+ public void ShortPropertyBlocks_ConsecutiveValuesDifferAndStayWellDistributed()
{
- var previous = BuildHash((1060584, "0"));
-
- for (var charges = 1; charges < 20000; charges++)
- {
- var current = BuildHash((1060584, charges.ToString()));
- Assert.NotEqual(previous, current);
- previous = current;
- }
- }
-
- [Fact]
- public void SmallPropertyBlocks_StayWellDistributed()
- {
- const int count = 20000;
+ const int count = 5000;
var withArgument = new HashSet();
var withoutArgument = new HashSet();
+ var previous = 0;
for (var i = 0; i < count; i++)
{
- withArgument.Add(BuildHash((1060584, i.ToString())));
+ var current = BuildHash((1060584, i.ToString()));
+ if (i > 0)
+ {
+ Assert.NotEqual(previous, current);
+ }
+
+ previous = current;
+ withArgument.Add(current);
withoutArgument.Add(BuildHash((1060000 + i, null)));
}
- // Birthday expects ~3 collisions over a 26-bit space; allow an order of magnitude so the
- // bound holds for any seed. A hash that stopped mixing collapses far past it.
- Assert.True(withArgument.Count >= count - 30, $"8-byte blocks: {withArgument.Count}/{count}");
- Assert.True(withoutArgument.Count >= count - 30, $"6-byte blocks: {withoutArgument.Count}/{count}");
+ // Birthday expects ~0.2 collisions over a 26-bit space; allow a few so the bound holds for
+ // any seed. A hash that stopped mixing collapses far past it.
+ Assert.True(withArgument.Count >= count - 5, $"8-byte blocks: {withArgument.Count}/{count}");
+ Assert.True(withoutArgument.Count >= count - 5, $"6-byte blocks: {withoutArgument.Count}/{count}");
}
[Fact]
diff --git a/Projects/Server.Tests/Tests/Serialization/AnchoredTimeTests.cs b/Projects/Server.Tests/Tests/Serialization/AnchoredTimeTests.cs
index a99b9850c..74edf09c3 100644
--- a/Projects/Server.Tests/Tests/Serialization/AnchoredTimeTests.cs
+++ b/Projects/Server.Tests/Tests/Serialization/AnchoredTimeTests.cs
@@ -1,6 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.IO;
using Xunit;
namespace Server.Tests;
@@ -107,83 +105,48 @@ public class AnchoredTimePersistenceTests
[Fact]
public void SaveStartAnchor_RebasesAnchoredTimestampsAtLoad()
{
- var previousAssemblies = AssemblyHandler.Assemblies;
- AssemblyHandler.Assemblies = [.. previousAssemblies ?? [], typeof(AnchoredEntity).Assembly];
+ // The scope also restores World.SaveStartTime, which this test backdates.
+ using var pipeline = new PersistencePipelineScope(
+ 2,
+ installWorkers: true,
+ visibleAssembly: typeof(AnchoredEntity).Assembly,
+ directoryPrefix: "muo-anchored"
+ );
+ var source = pipeline.Source;
+ var dir = pipeline.Directory;
- var source = new SerializationChunkSource();
- var workers = new SerializationThreadWorker[2];
- for (var i = 0; i < workers.Length; i++)
- {
- workers[i] = new SerializationThreadWorker(i, source);
- workers[i].AllocateHeap();
- }
+ var persistence = pipeline.Track(new AnchoredPersistence(2100));
- var previousWorkers = World._threadWorkers;
- World._threadWorkers = workers;
+ var lastRested = Core.Now - TimeSpan.FromMinutes(10);
+ var serial = (Serial)1u;
+ persistence.EntitiesBySerial[serial] = new AnchoredEntity(serial) { LastRested = lastRested };
+ persistence.RegisterType(typeof(AnchoredEntity));
- var previousSaveStart = World.SaveStartTime;
+ // Pretend the save started two hours ago, as if the server had been down since.
+ var downtime = TimeSpan.FromHours(2);
+ World.SaveStartTime = Core.Now - downtime;
- var persistence = new AnchoredPersistence(2100);
- AnchoredPersistence loaded = null;
+ pipeline.WakeWorkers();
- var dir = Path.Combine(Path.GetTempPath(), $"muo-anchored-{Guid.NewGuid():N}");
- Directory.CreateDirectory(dir);
+ source.SetOwner(persistence);
+ Assert.True(persistence.TrySnapshotEntries(out var slotCount));
+ source.PushSlotRanges(persistence, slotCount);
- try
- {
- var lastRested = Core.Now - TimeSpan.FromMinutes(10);
- var serial = (Serial)1u;
- persistence.EntitiesBySerial[serial] = new AnchoredEntity(serial) { LastRested = lastRested };
- persistence.RegisterType(typeof(AnchoredEntity));
+ pipeline.FlushAndSleepWorkers();
- // Pretend the save started two hours ago, as if the server had been down since.
- var downtime = TimeSpan.FromHours(2);
- World.SaveStartTime = Core.Now - downtime;
+ persistence.WriteSnapshot(dir);
+ persistence.PostWorldSave();
- foreach (var worker in workers)
- {
- worker.Wake();
- }
+ var loaded = pipeline.Track(new AnchoredPersistence(2101));
+ loaded.DeserializeIndexes(dir, null);
+ loaded.Deserialize(dir, null);
- source.SetOwner(persistence);
- Assert.True(persistence.TrySnapshotEntries(out var slotCount));
- source.PushSlotRanges(persistence, slotCount);
+ var entity = loaded.EntitiesBySerial[serial];
+ var expected = lastRested + downtime;
- source.Flush();
- foreach (var worker in workers)
- {
- worker.Sleep();
- }
-
- persistence.WriteSnapshot(dir);
- persistence.PostWorldSave();
-
- loaded = new AnchoredPersistence(2101);
- loaded.DeserializeIndexes(dir, null);
- loaded.Deserialize(dir, null);
-
- var entity = loaded.EntitiesBySerial[serial];
- var expected = lastRested + downtime;
-
- Assert.True(
- (entity.LastRested - expected).Duration() <= TimeSpan.FromSeconds(30),
- $"Anchored timestamp must re-base by the downtime; expected ~{expected}, got {entity.LastRested}."
- );
- }
- finally
- {
- World.SaveStartTime = previousSaveStart;
- persistence.Unregister();
- loaded?.Unregister();
-
- foreach (var worker in workers)
- {
- worker.Exit();
- }
-
- World._threadWorkers = previousWorkers;
- AssemblyHandler.Assemblies = previousAssemblies;
- Directory.Delete(dir, true);
- }
+ Assert.True(
+ (entity.LastRested - expected).Duration() <= TimeSpan.FromSeconds(30),
+ $"Anchored timestamp must re-base by the downtime; expected ~{expected}, got {entity.LastRested}."
+ );
}
}
diff --git a/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs b/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs
index 48ba16e55..c8c4522aa 100644
--- a/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs
+++ b/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs
@@ -1,6 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.IO;
using Xunit;
namespace Server.Tests;
@@ -72,103 +70,68 @@ public class GenericEntityPersistenceRoundTripTests
[Fact]
public void SnapshotRoundTripsThroughWorkersAndSegmentLogs()
{
- // The loader resolves types by hash through AssemblyHandler; make this test
- // assembly visible for the duration of the test.
- var previousAssemblies = AssemblyHandler.Assemblies;
- AssemblyHandler.Assemblies = [.. previousAssemblies ?? [], typeof(RoundTripEntity).Assembly];
+ // The loader resolves types by hash through AssemblyHandler, so this test assembly must be visible.
+ using var pipeline = new PersistencePipelineScope(
+ 3,
+ installWorkers: true,
+ visibleAssembly: typeof(RoundTripEntity).Assembly,
+ directoryPrefix: "muo-roundtrip"
+ );
+ var source = pipeline.Source;
+ var dir = pipeline.Directory;
- var source = new SerializationChunkSource();
- var workers = new SerializationThreadWorker[3];
- for (var i = 0; i < workers.Length; i++)
+ var persistence = pipeline.Track(new RoundTripPersistence(2000));
+
+ const int entityCount = 25_000;
+ var rng = new System.Random(0x5EED);
+
+ for (var i = 1; i <= entityCount; i++)
{
- workers[i] = new SerializationThreadWorker(i, source);
- workers[i].AllocateHeap();
+ var serial = (Serial)(uint)i;
+ persistence.EntitiesBySerial[serial] = new RoundTripEntity(serial)
+ {
+ Value = rng.Next(),
+ Name = i % 5 == 0 ? null : $"entity-{i}"
+ };
}
- var previousWorkers = World._threadWorkers;
- World._threadWorkers = workers;
+ persistence.RegisterType(typeof(RoundTripEntity));
- var persistence = new RoundTripPersistence(2000);
- RoundTripPersistence loaded = null;
+ // Freeze: what Persistence.SerializeAll + GenericEntityPersistence.Serialize do,
+ // against this test's chunk source instead of the world's.
+ pipeline.WakeWorkers();
- var dir = Path.Combine(Path.GetTempPath(), $"muo-roundtrip-{Guid.NewGuid():N}");
- Directory.CreateDirectory(dir);
+ source.SetOwner(persistence);
+ source.PushSingle(persistence);
+ Assert.True(persistence.TrySnapshotEntries(out var slotCount));
+ source.PushSlotRanges(persistence, slotCount);
- try
+ pipeline.FlushAndSleepWorkers();
+
+ // Background write phase: snapshot from the segment logs. v4 embeds the type
+ // table in the idx, so no SerializedTypes.db is produced or needed.
+ persistence.WriteSnapshot(dir);
+
+ persistence.PostWorldSave(); // releases the entries snapshot
+
+ // Load into a fresh persistence, like a server boot would.
+ var loaded = pipeline.Track(new RoundTripPersistence(2001));
+ loaded.DeserializeIndexes(dir, null);
+ loaded.Deserialize(dir, null);
+
+ Assert.True(loaded.SelfPayloadDeserialized);
+ Assert.Equal(persistence.EntitiesBySerial.Count, loaded.EntitiesBySerial.Count);
+
+ foreach (var (serial, original) in persistence.EntitiesBySerial)
{
- const int entityCount = 25_000;
- var rng = new System.Random(0x5EED);
-
- for (var i = 1; i <= entityCount; i++)
- {
- var serial = (Serial)(uint)i;
- persistence.EntitiesBySerial[serial] = new RoundTripEntity(serial)
- {
- Value = rng.Next(),
- Name = i % 5 == 0 ? null : $"entity-{i}"
- };
- }
-
- persistence.RegisterType(typeof(RoundTripEntity));
-
- // Freeze: what Persistence.SerializeAll + GenericEntityPersistence.Serialize do,
- // against this test's chunk source instead of the world's.
- foreach (var worker in workers)
- {
- worker.Wake();
- }
-
- source.SetOwner(persistence);
- source.PushSingle(persistence);
- Assert.True(persistence.TrySnapshotEntries(out var slotCount));
- source.PushSlotRanges(persistence, slotCount);
-
- // Mirrors World.PauseSerializationThreads
- source.Flush();
- foreach (var worker in workers)
- {
- worker.Sleep();
- }
-
- // Background write phase: snapshot from the segment logs. v4 embeds the type
- // table in the idx, so no SerializedTypes.db is produced or needed.
- persistence.WriteSnapshot(dir);
-
- persistence.PostWorldSave(); // releases the entries snapshot
-
- // Load into a fresh persistence, like a server boot would.
- loaded = new RoundTripPersistence(2001);
- loaded.DeserializeIndexes(dir, null);
- loaded.Deserialize(dir, null);
-
- Assert.True(loaded.SelfPayloadDeserialized);
- Assert.Equal(persistence.EntitiesBySerial.Count, loaded.EntitiesBySerial.Count);
-
- foreach (var (serial, original) in persistence.EntitiesBySerial)
- {
- var entity = loaded.EntitiesBySerial[serial];
- Assert.Equal(original.Value, entity.Value);
- Assert.Equal(original.Name, entity.Name);
- Assert.Equal(original.Created.Ticks, entity.Created.Ticks);
- }
-
- // Loading must hydrate the type table so the next save can write indexes.
- Assert.True(loaded.TryGetTypeIndex(typeof(RoundTripEntity), out _));
+ var entity = loaded.EntitiesBySerial[serial];
+ Assert.Equal(original.Value, entity.Value);
+ Assert.Equal(original.Name, entity.Name);
+ Assert.Equal(original.Created.Ticks, entity.Created.Ticks);
}
- finally
- {
- persistence.Unregister();
- loaded?.Unregister();
- foreach (var worker in workers)
- {
- worker.Exit();
- }
-
- World._threadWorkers = previousWorkers;
- AssemblyHandler.Assemblies = previousAssemblies;
- Directory.Delete(dir, true);
- }
+ // Loading must hydrate the type table so the next save can write indexes.
+ Assert.True(loaded.TryGetTypeIndex(typeof(RoundTripEntity), out _));
}
///
@@ -179,70 +142,44 @@ public class GenericEntityPersistenceRoundTripTests
[Fact]
public void SerializerException_IsRecordedOnTheWorker_AndTheDrainCompletes()
{
- var source = new SerializationChunkSource();
- var workers = new SerializationThreadWorker[2];
- for (var i = 0; i < workers.Length; i++)
+ using var pipeline = new PersistencePipelineScope(2);
+ var source = pipeline.Source;
+ var workers = pipeline.Workers;
+
+ var persistence = pipeline.Track(new RoundTripPersistence(2002));
+
+ for (var i = 1; i <= 100; i++)
{
- workers[i] = new SerializationThreadWorker(i, source);
- workers[i].AllocateHeap();
+ var serial = (Serial)(uint)i;
+ persistence.EntitiesBySerial[serial] = i == 50
+ ? new ThrowingRoundTripEntity(serial)
+ : new RoundTripEntity(serial) { Value = i, Name = $"entity-{i}" };
}
- var persistence = new RoundTripPersistence(2002);
+ pipeline.WakeWorkers();
- try
+ source.SetOwner(persistence);
+ Assert.True(persistence.TrySnapshotEntries(out var slotCount));
+ source.PushSlotRanges(persistence, slotCount);
+
+ pipeline.FlushAndSleepWorkers();
+
+ Exception error = null;
+ foreach (var worker in workers)
{
- for (var i = 1; i <= 100; i++)
- {
- var serial = (Serial)(uint)i;
- persistence.EntitiesBySerial[serial] = i == 50
- ? new ThrowingRoundTripEntity(serial)
- : new RoundTripEntity(serial) { Value = i, Name = $"entity-{i}" };
- }
-
- foreach (var worker in workers)
- {
- worker.Wake();
- }
-
- source.SetOwner(persistence);
- Assert.True(persistence.TrySnapshotEntries(out var slotCount));
- source.PushSlotRanges(persistence, slotCount);
- source.Flush();
-
- foreach (var worker in workers)
- {
- worker.Sleep();
- }
-
- Exception error = null;
- foreach (var worker in workers)
- {
- error ??= worker.Error;
- }
-
- Assert.IsType(error);
- persistence.PostWorldSave();
-
- // The next drain starts clean.
- foreach (var worker in workers)
- {
- worker.Wake();
- }
-
- foreach (var worker in workers)
- {
- worker.Sleep();
- Assert.Null(worker.Error);
- }
+ error ??= worker.Error;
}
- finally
- {
- persistence.Unregister();
- foreach (var worker in workers)
- {
- worker.Exit();
- }
+ Assert.IsType(error);
+ persistence.PostWorldSave();
+
+ // The next drain starts clean.
+ pipeline.WakeWorkers();
+
+ foreach (var worker in workers)
+ {
+ worker.Sleep();
+ Assert.Null(worker.Error);
}
}
@@ -253,60 +190,28 @@ public class GenericEntityPersistenceRoundTripTests
[Fact]
public void WriteSnapshot_FailsTheSave_InsteadOfDroppingASegment()
{
- var source = new SerializationChunkSource();
- var workers = new SerializationThreadWorker[2];
- for (var i = 0; i < workers.Length; i++)
+ using var pipeline = new PersistencePipelineScope(2, installWorkers: true, directoryPrefix: "muo-segmentfail");
+ var source = pipeline.Source;
+
+ var persistence = pipeline.Track(new RoundTripPersistence(2003));
+
+ for (var i = 1; i <= 100; i++)
{
- workers[i] = new SerializationThreadWorker(i, source);
- workers[i].AllocateHeap();
+ var serial = (Serial)(uint)i;
+ persistence.EntitiesBySerial[serial] = new RoundTripEntity(serial) { Value = i, Name = $"entity-{i}" };
}
- var previousWorkers = World._threadWorkers;
- World._threadWorkers = workers;
+ // Deliberately not registered: the writer cannot index the type.
- var persistence = new RoundTripPersistence(2003);
- var dir = Path.Combine(Path.GetTempPath(), $"muo-segmentfail-{Guid.NewGuid():N}");
- Directory.CreateDirectory(dir);
+ pipeline.WakeWorkers();
- try
- {
- for (var i = 1; i <= 100; i++)
- {
- var serial = (Serial)(uint)i;
- persistence.EntitiesBySerial[serial] = new RoundTripEntity(serial) { Value = i, Name = $"entity-{i}" };
- }
+ source.SetOwner(persistence);
+ Assert.True(persistence.TrySnapshotEntries(out var slotCount));
+ source.PushSlotRanges(persistence, slotCount);
- // Deliberately not registered: the writer cannot index the type.
+ pipeline.FlushAndSleepWorkers();
- foreach (var worker in workers)
- {
- worker.Wake();
- }
-
- source.SetOwner(persistence);
- Assert.True(persistence.TrySnapshotEntries(out var slotCount));
- source.PushSlotRanges(persistence, slotCount);
- source.Flush();
-
- foreach (var worker in workers)
- {
- worker.Sleep();
- }
-
- Assert.Throws(() => persistence.WriteSnapshot(dir));
- persistence.PostWorldSave();
- }
- finally
- {
- persistence.Unregister();
-
- foreach (var worker in workers)
- {
- worker.Exit();
- }
-
- World._threadWorkers = previousWorkers;
- Directory.Delete(dir, true);
- }
+ Assert.Throws(() => persistence.WriteSnapshot(pipeline.Directory));
+ persistence.PostWorldSave();
}
}
diff --git a/Projects/Server.Tests/Tests/Serialization/SerializationChunkSourceTests.cs b/Projects/Server.Tests/Tests/Serialization/SerializationChunkSourceTests.cs
index 69387f4e6..24fea88a7 100644
--- a/Projects/Server.Tests/Tests/Serialization/SerializationChunkSourceTests.cs
+++ b/Projects/Server.Tests/Tests/Serialization/SerializationChunkSourceTests.cs
@@ -199,114 +199,91 @@ public class SerializationChunkSourceTests
[Fact]
public void WorkersDrainAllEntitiesAndLogSegments()
{
- var source = new SerializationChunkSource();
- var owner = new TestPersistence { PayloadSize = 512 * 1024 };
- var workers = new SerializationThreadWorker[2];
- for (var i = 0; i < workers.Length; i++)
+ using var pipeline = new PersistencePipelineScope(2);
+ var source = pipeline.Source;
+ var workers = pipeline.Workers;
+ var owner = pipeline.Track(new TestPersistence { PayloadSize = 512 * 1024 });
+
+ var entities = new List();
+ for (var i = 0; i < 10_000; i++)
{
- workers[i] = new SerializationThreadWorker(i, source);
- workers[i].AllocateHeap();
+ entities.Add(new TestEntity { PayloadSize = 16 + i % 64 });
}
- try
+ pipeline.WakeWorkers();
+
+ // A large payload published as a dedicated single chunk (a persistence
+ // self-payload), interleaved with the bare entity stream.
+ source.SetOwner(owner);
+ for (var i = 0; i < entities.Count; i++)
{
- var entities = new List();
- for (var i = 0; i < 10_000; i++)
+ if (i == 5000)
{
- entities.Add(new TestEntity { PayloadSize = 16 + i % 64 });
+ source.PushSingle(owner);
}
- foreach (var worker in workers)
- {
- worker.Wake();
- }
+ source.Push(entities[i]);
+ }
- // A large payload published as a dedicated single chunk (a persistence
- // self-payload), interleaved with the bare entity stream.
- source.SetOwner(owner);
- for (var i = 0; i < entities.Count; i++)
+ pipeline.FlushAndSleepWorkers();
+
+ long totalEntities = 0;
+ long totalBytes = 0;
+ foreach (var worker in workers)
+ {
+ totalEntities += worker.EntitiesSerialized;
+ totalBytes += worker.BytesSerialized;
+ }
+
+ Assert.Equal(entities.Count + 1, totalEntities);
+
+ // The self-payload recorded its placement on the persistence itself.
+ var (selfThread, selfPosition, selfLength) = owner.Placement;
+ Assert.Equal(owner.PayloadSize, selfLength);
+ Assert.InRange(selfThread, (byte)0, (byte)(workers.Length - 1));
+ var selfHeap = workers[selfThread].GetHeap(selfPosition, selfLength);
+ Assert.Equal(0, selfHeap[0]);
+ Assert.Equal((selfLength - 1) & 0xFF, selfHeap[^1]);
+
+ // Every entity appears exactly once in the worker segment logs, with a
+ // consistent span on that worker's heap: positions are implicit (contiguous
+ // writes), identity comes from the buffer-entities log.
+ var seen = new HashSet();
+ long expectedBytes = owner.PayloadSize;
+ foreach (var e in entities)
+ {
+ expectedBytes += e.PayloadSize;
+ }
+
+ foreach (var worker in workers)
+ {
+ var lengths = worker.Lengths;
+ var bufferEntities = worker.BufferEntities;
+
+ foreach (var segment in worker.Segments)
{
- if (i == 5000)
+ Assert.Same(owner, segment.Owner);
+ Assert.Equal(-1, segment.SlotOffset);
+
+ var heapPos = (int)segment.HeapStart;
+ for (var i = 0; i < segment.RecordCount; i++)
{
- source.PushSingle(owner);
- }
+ var entity = (TestEntity)bufferEntities[segment.EntitiesStart + i];
+ var length = lengths[segment.LengthsStart + i];
- source.Push(entities[i]);
- }
+ Assert.True(seen.Add(entity));
+ Assert.Equal(entity.PayloadSize, length);
- // Mirrors World.PauseSerializationThreads
- source.Flush();
- foreach (var worker in workers)
- {
- worker.Sleep();
- }
+ var heap = workers[Array.IndexOf(workers, worker)].GetHeap(heapPos, length);
+ Assert.Equal(0, heap[0]);
+ Assert.Equal((length - 1) & 0xFF, heap[^1]);
- long totalEntities = 0;
- long totalBytes = 0;
- foreach (var worker in workers)
- {
- totalEntities += worker.EntitiesSerialized;
- totalBytes += worker.BytesSerialized;
- }
-
- Assert.Equal(entities.Count + 1, totalEntities);
-
- // The self-payload recorded its placement on the persistence itself.
- var (selfThread, selfPosition, selfLength) = owner.Placement;
- Assert.Equal(owner.PayloadSize, selfLength);
- Assert.InRange(selfThread, (byte)0, (byte)(workers.Length - 1));
- var selfHeap = workers[selfThread].GetHeap(selfPosition, selfLength);
- Assert.Equal(0, selfHeap[0]);
- Assert.Equal((selfLength - 1) & 0xFF, selfHeap[^1]);
-
- // Every entity appears exactly once in the worker segment logs, with a
- // consistent span on that worker's heap: positions are implicit (contiguous
- // writes), identity comes from the buffer-entities log.
- var seen = new HashSet();
- long expectedBytes = owner.PayloadSize;
- foreach (var e in entities)
- {
- expectedBytes += e.PayloadSize;
- }
-
- foreach (var worker in workers)
- {
- var lengths = worker.Lengths;
- var bufferEntities = worker.BufferEntities;
-
- foreach (var segment in worker.Segments)
- {
- Assert.Same(owner, segment.Owner);
- Assert.Equal(-1, segment.SlotOffset);
-
- var heapPos = (int)segment.HeapStart;
- for (var i = 0; i < segment.RecordCount; i++)
- {
- var entity = (TestEntity)bufferEntities[segment.EntitiesStart + i];
- var length = lengths[segment.LengthsStart + i];
-
- Assert.True(seen.Add(entity));
- Assert.Equal(entity.PayloadSize, length);
-
- var heap = workers[Array.IndexOf(workers, worker)].GetHeap(heapPos, length);
- Assert.Equal(0, heap[0]);
- Assert.Equal((length - 1) & 0xFF, heap[^1]);
-
- heapPos += length;
- }
+ heapPos += length;
}
}
+ }
- Assert.Equal(entities.Count, seen.Count);
- Assert.Equal(expectedBytes, totalBytes);
- }
- finally
- {
- owner.Unregister();
- foreach (var worker in workers)
- {
- worker.Exit();
- }
- }
+ Assert.Equal(entities.Count, seen.Count);
+ Assert.Equal(expectedBytes, totalBytes);
}
}
diff --git a/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs b/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs
index 6c841f9f8..376fd661e 100644
--- a/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs
+++ b/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs
@@ -1,3 +1,4 @@
+using System.Reflection;
using Xunit;
namespace Server.Tests;
@@ -5,20 +6,38 @@ namespace Server.Tests;
[Collection("Sequential Server Tests")]
public class VirtualSerialTests
{
+ // Walking up from ResetVirtualSerial takes ~17.9 million draws; start just short of the wrap.
+ private const uint DrawsBeforeMax = 1000;
+
+ private static readonly FieldInfo _nextVirtualSerial =
+ typeof(World).GetField("_nextVirtualSerial", BindingFlags.NonPublic | BindingFlags.Static);
+
[Fact]
public void TestNewVirtualGetsAndRollover()
{
- // Acquire virtual serials until we hit the max
- var lastSerial = World.NewVirtual;
- do
- {
- var virtualSerial = World.NewVirtual;
- Assert.Equal(lastSerial + 1, virtualSerial);
- lastSerial = (Serial) (uint) virtualSerial;
- } while (lastSerial != World.MaxVirtualSerial);
+ Assert.NotNull(_nextVirtualSerial);
- // Next one should be MaxItemSerial (the first VirtualSerial) due to rollover
- var nextSerial = World.NewVirtual;
- Assert.Equal(nextSerial, (Serial)World.ResetVirtualSerial);
+ var saved = (uint)_nextVirtualSerial.GetValue(null);
+ _nextVirtualSerial.SetValue(null, World.MaxVirtualSerial - DrawsBeforeMax);
+
+ try
+ {
+ // Acquire virtual serials until we hit the max
+ var lastSerial = World.NewVirtual;
+ do
+ {
+ var virtualSerial = World.NewVirtual;
+ Assert.Equal(lastSerial + 1, virtualSerial);
+ lastSerial = (Serial)(uint)virtualSerial;
+ } while (lastSerial != World.MaxVirtualSerial);
+
+ // Next one should be MaxItemSerial (the first VirtualSerial) due to rollover
+ var nextSerial = World.NewVirtual;
+ Assert.Equal(nextSerial, (Serial)World.ResetVirtualSerial);
+ }
+ finally
+ {
+ _nextVirtualSerial.SetValue(null, saved);
+ }
}
}
diff --git a/Projects/UOContent.Tests/Helpers/AiTestStubs.cs b/Projects/UOContent.Tests/Helpers/AiTestStubs.cs
new file mode 100644
index 000000000..9d954c062
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/AiTestStubs.cs
@@ -0,0 +1,42 @@
+using Server.Mobiles;
+
+namespace Server.Tests;
+
+///
+/// A plain : walker, target or bystander. Deriving straight from Mobile rather
+/// than BaseCreature is the point: MovementImpl sees no creature capabilities (no swim, no fly, no
+/// door-opening), which is exactly the walker the step cache bakes for.
+///
+public sealed class WalkerStub : Mobile
+{
+ public WalkerStub() => Body = 0xC9;
+}
+
+///
+/// A BaseCreature built through the Serial constructor, which skips the NPCSpeeds lookup and
+/// leaves AIObject unset: call and attach an AI by hand.
+///
+public sealed class BareCreatureStub : BaseCreature
+{
+ public BareCreatureStub(Serial serial) : base(serial) => Body = 0xC9;
+}
+
+public static class TestTiles
+{
+ ///
+ /// The first item ID whose tile data is an impassable surface, so a test can wall a cell off
+ /// without pinning a specific graphic. 0 when tile data is not loaded.
+ ///
+ public static ushort FirstImpassableItemId()
+ {
+ for (ushort id = 1; id < TileData.MaxItemValue; id++)
+ {
+ if (TileData.ItemTable[id].ImpassableSurface)
+ {
+ return id;
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/Projects/UOContent.Tests/Helpers/CommandTestHarness.cs b/Projects/UOContent.Tests/Helpers/CommandTestHarness.cs
new file mode 100644
index 000000000..61ee1f6b3
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/CommandTestHarness.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using Server.Commands;
+using Server.Items;
+
+namespace Server.Tests;
+
+///
+/// Shared setup for the generic-command compiler tests: teleporters to run conditions, sorts and
+/// distincts against, deleted on dispose, and property bindings resolved for reading.
+///
+internal sealed class CommandTestHarness : IDisposable
+{
+ private readonly List- _items = [];
+
+ public T Track(T item) where T : Item
+ {
+ _items.Add(item);
+ return item;
+ }
+
+ public SkillTeleporter Teleporter(TextDefinition message) => Track(new SkillTeleporter { Message = message });
+
+ public static Property Bind(Type type, string binding)
+ {
+ var prop = new Property(binding);
+ prop.BindTo(type, PropertyAccess.Read);
+ return prop;
+ }
+
+ public void Dispose()
+ {
+ for (var i = 0; i < _items.Count; i++)
+ {
+ _items[i].Delete();
+ }
+
+ _items.Clear();
+ }
+}
diff --git a/Projects/UOContent.Tests/Helpers/MobileTestHarness.cs b/Projects/UOContent.Tests/Helpers/MobileTestHarness.cs
new file mode 100644
index 000000000..30b86f6d6
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/MobileTestHarness.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using Server.Mobiles;
+
+namespace Server.Tests;
+
+///
+/// Owns the mobiles and items a test creates and deletes them on dispose, so a failed assertion
+/// cannot leave entities registered in the shared world. Hold one per test class instance and
+/// dispose it from the class's .
+///
+internal sealed class MobileTestHarness : IDisposable
+{
+ private readonly List _mobiles = [];
+ private readonly List
- _items = [];
+
+ public T Track(T mobile) where T : Mobile
+ {
+ _mobiles.Add(mobile);
+ return mobile;
+ }
+
+ public T TrackItem(T item) where T : Item
+ {
+ _items.Add(item);
+ return item;
+ }
+
+ ///
+ /// A player registered in the world table, so serialized references to it resolve on load.
+ ///
+ public PlayerMobile NewPlayer()
+ {
+ var pm = new PlayerMobile(World.NewMobile);
+ pm.DefaultMobileInit();
+ World.AddEntity(pm);
+ return Track(pm);
+ }
+
+ public TestCreature NewCreature() => Track(new TestCreature());
+
+ public void Dispose()
+ {
+ for (var i = 0; i < _mobiles.Count; i++)
+ {
+ _mobiles[i].Delete();
+ }
+
+ for (var i = 0; i < _items.Count; i++)
+ {
+ _items[i].Delete();
+ }
+ }
+}
+
+///
+/// A plain melee creature. Its speeds are pinned so tests do not depend on the npc-speeds table.
+///
+internal class TestCreature : BaseCreature
+{
+ public TestCreature() : base(AIType.AI_Melee) => Body = 0xC9;
+
+ public TestCreature(Serial serial) : base(serial) => Body = 0xC9;
+
+ public DateTime SummonEndValue => SummonEnd;
+
+ public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
+ {
+ activeSpeed = 0.3;
+ passiveSpeed = 0.6;
+ }
+
+ public override void GetMoveSpeeds(out double activeMoveSpeed, out double passiveMoveSpeed)
+ {
+ activeMoveSpeed = 0.6;
+ passiveMoveSpeed = 1.2;
+ }
+}
+
+///
+/// A vendor with no outfit and pinned speeds. Subclasses stock it by overriding
+/// , which runs from the base constructor.
+///
+internal class TestVendor : BaseVendor
+{
+ // Initialized before the base constructor calls InitSBInfo.
+ private readonly List _sbInfos = [];
+
+ public TestVendor() : base("the stub")
+ {
+ }
+
+ public TestVendor(Serial serial) : base(serial)
+ {
+ }
+
+ protected override List SBInfos => _sbInfos;
+
+ public override void InitSBInfo() => AddSBInfos(_sbInfos);
+
+ protected virtual void AddSBInfos(List sbInfos)
+ {
+ }
+
+ public override void InitOutfit()
+ {
+ }
+
+ public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
+ {
+ activeSpeed = 0.3;
+ passiveSpeed = 0.6;
+ }
+}
diff --git a/Projects/UOContent.Tests/Helpers/StepCacheStateScope.cs b/Projects/UOContent.Tests/Helpers/StepCacheStateScope.cs
new file mode 100644
index 000000000..39e22fb63
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/StepCacheStateScope.cs
@@ -0,0 +1,45 @@
+using System;
+using Server.Engines.Pathing.Cache;
+using Server.Systems.FeatureFlags;
+
+namespace Server.Tests;
+
+///
+/// Snapshots the pathfinding cache's tuning knobs and feature flag, and restores them on dispose
+/// along with empty caches. StepCache and MultiMaskCache are process-wide singletons and
+/// leaves the knobs alone, so a test that tunes one would otherwise
+/// leak it into every test that runs after it.
+///
+internal sealed class StepCacheStateScope : IDisposable
+{
+ private readonly int _missPromotionThreshold;
+ private readonly uint _missPromotionWindowMs;
+ private readonly int _maxResidentChunks;
+ private readonly bool _preloadOnLazyOpen;
+ private readonly bool _cacheEnabled;
+
+ public StepCacheStateScope()
+ {
+ var cache = StepCache.Instance;
+ _missPromotionThreshold = cache.MissPromotionThreshold;
+ _missPromotionWindowMs = cache.MissPromotionWindowMs;
+ _maxResidentChunks = cache.MaxResidentChunks;
+ _preloadOnLazyOpen = cache.PreloadOnLazyOpen;
+ _cacheEnabled = ContentFeatureFlags.BitmapPathfindingCache;
+ }
+
+ public void Dispose()
+ {
+ var cache = StepCache.Instance;
+
+ // Clear first: it closes any lazy reader a test opened under the knobs it set.
+ cache.Clear();
+ MultiMaskCache.Instance.Clear();
+
+ cache.MissPromotionThreshold = _missPromotionThreshold;
+ cache.MissPromotionWindowMs = _missPromotionWindowMs;
+ cache.MaxResidentChunks = _maxResidentChunks;
+ cache.PreloadOnLazyOpen = _preloadOnLazyOpen;
+ ContentFeatureFlags.BitmapPathfindingCache = _cacheEnabled;
+ }
+}
diff --git a/Projects/UOContent.Tests/Helpers/TempDirectory.cs b/Projects/UOContent.Tests/Helpers/TempDirectory.cs
new file mode 100644
index 000000000..a88f54d19
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/TempDirectory.cs
@@ -0,0 +1,44 @@
+using System;
+using System.IO;
+
+namespace Server.Tests;
+
+///
+/// A uniquely named directory under the system temp path, deleted with its contents on dispose so a
+/// failing assertion cannot leave files behind.
+///
+internal sealed class TempDirectory : IDisposable
+{
+ public TempDirectory(string prefix = "modernuo-test")
+ {
+ Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"{prefix}-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(Path);
+ }
+
+ public string Path { get; }
+
+ public string Combine(string relativePath) => System.IO.Path.Combine(Path, relativePath);
+
+ /// Writes to and returns its full path.
+ public string WriteFile(string relativePath, string content)
+ {
+ var path = Combine(relativePath);
+ File.WriteAllText(path, content);
+ return path;
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ if (Directory.Exists(Path))
+ {
+ Directory.Delete(Path, true);
+ }
+ }
+ catch (IOException)
+ {
+ // A handle still open on Windows; the OS temp cleaner reclaims it.
+ }
+ }
+}
diff --git a/Projects/UOContent.Tests/Helpers/TimerWheelScope.cs b/Projects/UOContent.Tests/Helpers/TimerWheelScope.cs
new file mode 100644
index 000000000..daabd3188
--- /dev/null
+++ b/Projects/UOContent.Tests/Helpers/TimerWheelScope.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Reflection;
+
+namespace Server.Tests;
+
+///
+/// Gives a test its own simulated clock and an empty timer wheel, and puts the process-wide ones
+/// back on dispose. Core's tick, Core.Now and the wheel are statics shared by every test in the
+/// host, so a test that resets them without restoring hands its clock to whatever runs next.
+///
+///
+/// Timers started inside the scope are stopped on dispose; timers that were scheduled before it
+/// resume on the restored wheel.
+///
+internal sealed class TimerWheelScope : IDisposable
+{
+ private const BindingFlags PrivateStatic = BindingFlags.NonPublic | BindingFlags.Static;
+
+ private static readonly FieldInfo _ringsField = GetTimerField("_rings");
+ private static readonly FieldInfo _ringIndexesField = GetTimerField("_ringIndexes");
+ private static readonly FieldInfo _lastTickTurnedField = GetTimerField("_lastTickTurned");
+
+ private readonly long _savedTick;
+ private readonly DateTime _savedNow;
+ private readonly Timer[][] _savedRings;
+ private readonly int[] _savedRingIndexes;
+ private readonly long _savedLastTickTurned;
+ private bool _disposed;
+
+ public TimerWheelScope(long startTick = 0)
+ {
+ _savedTick = Core._tickCount;
+ _savedNow = Core._now;
+ _savedRings = (Timer[][])Rings.Clone();
+ _savedRingIndexes = (int[])RingIndexes.Clone();
+ _savedLastTickTurned = (long)_lastTickTurnedField.GetValue(null)!;
+
+ Core._tickCount = startTick;
+ Timer.Init(startTick);
+ }
+
+ private static Timer[][] Rings => (Timer[][])_ringsField.GetValue(null)!;
+
+ private static int[] RingIndexes => (int[])_ringIndexesField.GetValue(null)!;
+
+ /// The wheel's tick rate: one turn per call keeps it in lockstep with Core.TickCount.
+ public static int Step => Timer.TickRate;
+
+ /// Advances the clock by , turning the wheel every step.
+ public static void RunFor(long ms)
+ {
+ var deadline = Core._tickCount + ms;
+
+ while (Core._tickCount - deadline < 0)
+ {
+ Core._tickCount += Step;
+ Timer.Slice(Core._tickCount);
+ }
+ }
+
+ ///
+ /// Advances the clock step by step until holds or
+ /// has passed. Returns the condition's final value.
+ ///
+ public static bool RunUntil(Func condition, long maxMs)
+ {
+ var deadline = Core._tickCount + maxMs;
+
+ while (Core._tickCount - deadline < 0)
+ {
+ if (condition())
+ {
+ return true;
+ }
+
+ Core._tickCount += Step;
+ Timer.Slice(Core._tickCount);
+ }
+
+ return condition();
+ }
+
+ /// Jumps the tick and Core.Now forward together, then fires everything that came due.
+ public static void Advance(TimeSpan time)
+ {
+ Core._now += time;
+ Core._tickCount += (long)time.TotalMilliseconds;
+ Timer.Slice(Core._tickCount);
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ StopAll(Rings);
+
+ var rings = Rings;
+ for (var i = 0; i < rings.Length; i++)
+ {
+ rings[i] = _savedRings[i];
+ }
+
+ Array.Copy(_savedRingIndexes, RingIndexes, _savedRingIndexes.Length);
+ _lastTickTurnedField.SetValue(null, _savedLastTickTurned);
+
+ Core._tickCount = _savedTick;
+ Core._now = _savedNow;
+ }
+
+ // Stop() unlinks the slot's head and promotes the next timer, so draining heads empties a slot.
+ private static void StopAll(Timer[][] rings)
+ {
+ for (var i = 0; i < rings.Length; i++)
+ {
+ var ring = rings[i];
+ for (var slot = 0; slot < ring.Length; slot++)
+ {
+ Timer head;
+ while ((head = ring[slot]) != null)
+ {
+ head.Stop();
+
+ if (ring[slot] == head)
+ {
+ // A head that was already stopped never unlinks itself.
+ ring[slot] = null;
+ }
+ }
+ }
+ }
+ }
+
+ private static FieldInfo GetTimerField(string name) =>
+ typeof(Timer).GetField(name, PrivateStatic)
+ ?? throw new InvalidOperationException($"Timer.{name} not found via reflection; the timer wheel was refactored.");
+}
diff --git a/Projects/UOContent.Tests/Tests/Accounting/AccountPasswordTests.cs b/Projects/UOContent.Tests/Tests/Accounting/AccountPasswordTests.cs
index 29a1b818f..c0901ca48 100644
--- a/Projects/UOContent.Tests/Tests/Accounting/AccountPasswordTests.cs
+++ b/Projects/UOContent.Tests/Tests/Accounting/AccountPasswordTests.cs
@@ -1,6 +1,7 @@
using System;
using Server.Accounting;
using Server.Accounting.Security;
+using Server.Tests.Accounting.Security;
using Xunit;
namespace Server.Tests.Accounting;
@@ -64,7 +65,9 @@ public class AccountPasswordTests : IDisposable
"$argon2i$v=19$m=8192,t=3,p=1$LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw";
Assert.True(account.CheckPassword(Password));
- Assert.StartsWith("$argon2id$v=19$m=16384,t=1,p=1$", account.Password);
+ // The cost parameters are shard-tunable; the variant and version are not.
+ Assert.StartsWith("$argon2id$v=19$", account.Password);
+ Assert.StartsWith(Argon2TestParameters.CurrentPrefix, account.Password);
// Already current: verifying again must not rewrite the hash.
var afterFirst = account.Password;
diff --git a/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs b/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs
index df499509e..3412feab9 100644
--- a/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs
+++ b/Projects/UOContent.Tests/Tests/Accounting/PasswordWorkerTests.cs
@@ -1,7 +1,9 @@
using System;
+using System.Collections.Generic;
using System.Threading;
using Server.Accounting;
using Server.Accounting.Security;
+using Server.Tests.Accounting.Security;
using Xunit;
namespace Server.Tests.Accounting;
@@ -13,12 +15,27 @@ public class PasswordWorkerTests : IDisposable
private readonly PasswordProtectionAlgorithm _originalAlgorithm = AccountSecurity.CurrentAlgorithm;
+ // The constructor registers each account in the process-wide Accounts table.
+ private readonly List _created = [];
+
public PasswordWorkerTests() => AccountSecurity.CurrentAlgorithm = PasswordProtectionAlgorithm.Argon2;
- public void Dispose() => AccountSecurity.CurrentAlgorithm = _originalAlgorithm;
+ public void Dispose()
+ {
+ for (var i = 0; i < _created.Count; i++)
+ {
+ _created[i].Delete();
+ }
- private static Account CreateAccount(string username) =>
- Accounts.GetAccount(username) as Account ?? new Account(username, Password);
+ AccountSecurity.CurrentAlgorithm = _originalAlgorithm;
+ }
+
+ private Account CreateAccount(string username)
+ {
+ var account = new Account(username, Password);
+ _created.Add(account);
+ return account;
+ }
///
/// Enqueues work, then pumps the loop context until or the deadline.
@@ -136,7 +153,8 @@ public class PasswordWorkerTests : IDisposable
var outcome = PasswordWorker.ComputeInline(JobFor(account, Password));
Assert.True(outcome.Verified);
- Assert.StartsWith("$argon2id$v=19$m=16384,t=1,p=1$", outcome.Hash);
+ Assert.StartsWith("$argon2id$v=19$", outcome.Hash);
+ Assert.StartsWith(Argon2TestParameters.CurrentPrefix, outcome.Hash);
}
[Fact]
diff --git a/Projects/UOContent.Tests/Tests/Accounting/Security/Argon2TestParameters.cs b/Projects/UOContent.Tests/Tests/Accounting/Security/Argon2TestParameters.cs
new file mode 100644
index 000000000..8e62573ac
--- /dev/null
+++ b/Projects/UOContent.Tests/Tests/Accounting/Security/Argon2TestParameters.cs
@@ -0,0 +1,55 @@
+using System;
+using Server.Accounting.Security;
+
+namespace Server.Tests.Accounting.Security;
+
+///
+/// The PHC parameter prefix the configured writes, read from
+/// a real hash so a shard that tunes the Argon2 cost does not break assertions about the format.
+///
+internal static class Argon2TestParameters
+{
+ /// For example $argon2id$v=19$m=16384,t=1,p=1$, ending at the salt.
+ public static readonly string CurrentPrefix = ReadPrefix();
+
+ public static string Type => CurrentPrefix.Split('$')[1];
+
+ public static string Version => CurrentPrefix.Split('$')[2];
+
+ public static int Memory => ReadCost('m');
+
+ public static int Time => ReadCost('t');
+
+ public static int Parallelism => ReadCost('p');
+
+ public static string BuildPrefix(string type, int memory, int time, int parallelism) =>
+ $"${type}${Version}$m={memory},t={time},p={parallelism}$";
+
+ private static string ReadPrefix()
+ {
+ var hash = Argon2PasswordProtection.Instance.EncryptPassword("prefix-probe");
+
+ // $type$version$params$salt$digest: the prefix runs through the fourth '$'.
+ var end = 0;
+ for (var i = 0; i < 4; i++)
+ {
+ end = hash.IndexOf('$', end) + 1;
+ }
+
+ return hash[..end];
+ }
+
+ private static int ReadCost(char key)
+ {
+ var costs = CurrentPrefix.Split('$')[3].Split(',');
+ for (var i = 0; i < costs.Length; i++)
+ {
+ if (costs[i][0] == key)
+ {
+ return int.Parse(costs[i].AsSpan(2));
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs b/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs
index dc16a7f5b..382a4bc0d 100644
--- a/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs
+++ b/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs
@@ -113,34 +113,60 @@ public class PasswordProtectionTest
Assert.False(Argon2PasswordProtection.Instance.ValidatePassword(LegacyArgon2iHash, "wrong"));
}
- [Theory]
- // type, memory, time, parallelism -> expected NeedsRehash
- [InlineData("argon2id", 16384, 1, 1, false)] // current defaults
- [InlineData("argon2i", 8192, 3, 1, true)] // the old shipping default
- [InlineData("argon2id", 8192, 1, 1, true)] // right type, stale memory
- [InlineData("argon2id", 16384, 3, 1, true)] // right type, stale iterations
- [InlineData("argon2id", 16384, 1, 2, true)] // right type, stale parallelism
- [InlineData("argon2i", 16384, 1, 1, true)] // right cost, stale type
- public void Argon2_NeedsRehash_ComparesTypeAndCost(
- string type, int memory, int time, int parallelism, bool expected
- )
- {
- var hash = $"${type}$v=19$m={memory},t={time},p={parallelism}$" +
- "LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw";
+ private const string SaltAndDigest = "LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw";
- Assert.Equal(expected, Argon2PasswordProtection.Instance.NeedsRehash(hash));
+ [Fact]
+ public void Argon2_NeedsRehash_IsFalseForCurrentParameters()
+ {
+ var hash = Argon2TestParameters.CurrentPrefix + SaltAndDigest;
+
+ Assert.False(Argon2PasswordProtection.Instance.NeedsRehash(hash));
+ }
+
+ // Each case differs from the configured parameters in exactly one field, so the test holds
+ // whatever cost a shard configures.
+ [Theory]
+ [InlineData("type")]
+ [InlineData("memory")]
+ [InlineData("time")]
+ [InlineData("parallelism")]
+ public void Argon2_NeedsRehash_ComparesTypeAndCost(string staleField)
+ {
+ var type = Argon2TestParameters.Type;
+ var memory = Argon2TestParameters.Memory;
+ var time = Argon2TestParameters.Time;
+ var parallelism = Argon2TestParameters.Parallelism;
+ var otherType = type == "argon2id" ? "argon2i" : "argon2id";
+
+ (type, memory, time, parallelism) = staleField switch
+ {
+ "type" => (otherType, memory, time, parallelism),
+ "memory" => (type, memory * 2, time, parallelism),
+ "time" => (type, memory, time + 1, parallelism),
+ _ => (type, memory, time, parallelism + 1)
+ };
+
+ var hash = Argon2TestParameters.BuildPrefix(type, memory, time, parallelism) + SaltAndDigest;
+
+ Assert.True(Argon2PasswordProtection.Instance.NeedsRehash(hash));
+ }
+
+ [Fact]
+ public void Argon2_NeedsRehash_IsTrueForLegacyShippingDefault()
+ {
+ Assert.True(Argon2PasswordProtection.Instance.NeedsRehash(LegacyArgon2iHash));
}
// Digest and salt lengths are decoded base64 sizes rather than parameter-list entries, so they
// need their own literals. Current type and cost throughout; only a length differs.
[Theory]
// 16-byte digest: 22 base64 chars instead of the 43 a 32-byte digest encodes to.
- [InlineData("$argon2id$v=19$m=16384,t=1,p=1$LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4g")]
+ [InlineData("LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4g")]
// 8-byte salt: 11 base64 chars instead of the 22 a 16-byte salt encodes to.
- [InlineData("$argon2id$v=19$m=16384,t=1,p=1$LD1XJz7P3wQ$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw")]
- public void Argon2_NeedsRehash_ComparesSaltAndDigestLengths(string hash)
+ [InlineData("LD1XJz7P3wQ$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw")]
+ public void Argon2_NeedsRehash_ComparesSaltAndDigestLengths(string saltAndDigest)
{
- Assert.True(Argon2PasswordProtection.Instance.NeedsRehash(hash));
+ Assert.True(Argon2PasswordProtection.Instance.NeedsRehash(Argon2TestParameters.CurrentPrefix + saltAndDigest));
}
[Theory]
diff --git a/Projects/UOContent.Tests/Tests/Commands/ChainedBindingSortTests.cs b/Projects/UOContent.Tests/Tests/Commands/ChainedBindingSortTests.cs
index 80e664647..dbeba969e 100644
--- a/Projects/UOContent.Tests/Tests/Commands/ChainedBindingSortTests.cs
+++ b/Projects/UOContent.Tests/Tests/Commands/ChainedBindingSortTests.cs
@@ -4,6 +4,7 @@ using Server;
using Server.Commands;
using Server.Commands.Generic;
using Server.Items;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Commands;
@@ -14,31 +15,13 @@ namespace UOContent.Tests.Commands;
[Collection("Sequential UOContent Tests")]
public class ChainedBindingSortTests : IDisposable
{
- private readonly List
- _items = [];
+ private readonly CommandTestHarness _harness = new();
- public void Dispose()
- {
- for (var i = 0; i < _items.Count; i++)
- {
- _items[i].Delete();
- }
+ public void Dispose() => _harness.Dispose();
- _items.Clear();
- }
+ private SkillTeleporter Teleporter(TextDefinition message) => _harness.Teleporter(message);
- private SkillTeleporter Teleporter(TextDefinition message)
- {
- var tp = new SkillTeleporter { Message = message };
- _items.Add(tp);
- return tp;
- }
-
- private static Property Bind(string binding)
- {
- var prop = new Property(binding);
- prop.BindTo(typeof(SkillTeleporter), PropertyAccess.Read);
- return prop;
- }
+ private static Property Bind(string binding) => CommandTestHarness.Bind(typeof(SkillTeleporter), binding);
private static IComparer
[Collection("Sequential Pathfinding Tests")]
-public class BitmapAStarAlgorithmTests
+public class BitmapAStarAlgorithmTests : IDisposable
{
private readonly ITestOutputHelper _output;
+ private readonly StepCacheStateScope _cacheState = new();
public BitmapAStarAlgorithmTests(ITestOutputHelper output)
{
_output = output;
}
+ public void Dispose() => _cacheState.Dispose();
+
[Theory]
// Pinned cell (1500, 1600, z=10): mask=0xC1 → N, W, NW walkable.
// Use start.Z for goal.Z so destNode lands in the same Z plane the algorithm reaches
@@ -33,22 +37,27 @@ public class BitmapAStarAlgorithmTests
public void DefaultWalker_FindsPath_ViaCacheFastPath(int sx, int sy, int gx, int gy)
{
StepCache.Instance.Clear();
+ StepCache.Instance.MissPromotionThreshold = 1;
var map = Map.Maps[1];
Assert.NotNull(map);
- var stub = new DefaultWalkerStub();
+ var stub = new WalkerStub();
map.GetAverageZ(sx, sy, out _, out var startZ, out _);
var start = new Point3D(sx, sy, (sbyte)startZ);
var goal = new Point3D(gx, gy, (sbyte)startZ);
stub.MoveToWorld(start, map);
+ var statsBefore = StepCache.Instance.GetStats();
var result = BitmapAStarAlgorithm.Instance.Find(stub, map, start, goal);
+ var statsAfter = StepCache.Instance.GetStats();
stub.Delete();
Assert.NotNull(result);
Assert.NotEmpty(result);
+ Assert.True(statsAfter.BuildsTotal > statsBefore.BuildsTotal,
+ "a default walker should expand through the cache, not the slow path");
_output.WriteLine($"default-walker ({sx},{sy})->({gx},{gy}): {result.Length} steps");
}
@@ -62,7 +71,7 @@ public class BitmapAStarAlgorithmTests
var map = Map.Maps[1];
Assert.NotNull(map);
- var stub = new SwimmingStub(World.NewMobile);
+ var stub = new BareCreatureStub(World.NewMobile);
stub.DefaultMobileInit();
stub.CanSwim = true; // overlay route — cache + (walkMask | wetMask&canSwim)
map.GetAverageZ(sx, sy, out _, out var startZ, out _);
@@ -104,13 +113,13 @@ public class BitmapAStarAlgorithmTests
var start = new Point3D(sx, sy, (sbyte)startZ);
var goal = new Point3D(gx, gy, (sbyte)startZ);
- var walker = new DefaultWalkerStub();
+ var walker = new WalkerStub();
walker.MoveToWorld(start, map);
// Living blocker on the only direct-line cell. CanMoveOver returns false for an
// alive non-staff mobile, so the dynamic-obstacle pass must reject this cell.
map.GetAverageZ(blockX, blockY, out _, out var blockZ, out _);
- var blocker = new DefaultWalkerStub();
+ var blocker = new WalkerStub();
blocker.MoveToWorld(new Point3D(blockX, blockY, (sbyte)blockZ), map);
var result = BitmapAStarAlgorithm.Instance.Find(walker, map, start, goal);
@@ -146,24 +155,14 @@ public class BitmapAStarAlgorithmTests
var blockX = 1499;
var blockY = 1599;
- // Find any ItemID whose TileData has ImpassableSurface so the dynamic pass
- // rejects the cell. Pinning to a specific ID would couple the test to UO art data.
- ushort blockerItemId = 0;
- for (ushort id = 1; id < TileData.MaxItemValue; id++)
- {
- if (TileData.ItemTable[id].ImpassableSurface)
- {
- blockerItemId = id;
- break;
- }
- }
+ var blockerItemId = TestTiles.FirstImpassableItemId();
Assert.NotEqual(0, blockerItemId);
map.GetAverageZ(sx, sy, out _, out var startZ, out _);
var start = new Point3D(sx, sy, (sbyte)startZ);
var goal = new Point3D(gx, gy, (sbyte)startZ);
- var walker = new DefaultWalkerStub();
+ var walker = new WalkerStub();
walker.MoveToWorld(start, map);
map.GetAverageZ(blockX, blockY, out _, out var blockZ, out _);
@@ -197,28 +196,20 @@ public class BitmapAStarAlgorithmTests
var map = Map.Maps[1];
Assert.NotNull(map);
- var stub = new DefaultWalkerStub();
+ var stub = new WalkerStub();
map.GetAverageZ(1500, 1600, out _, out var startZ, out _);
var start = new Point3D(1500, 1600, (sbyte)startZ);
var goal = new Point3D(1498, 1598, (sbyte)startZ);
stub.MoveToWorld(start, map);
var statsBefore = StepCache.Instance.GetStats();
- var prevFlag = ContentFeatureFlags.BitmapPathfindingCache;
- try
- {
- ContentFeatureFlags.BitmapPathfindingCache = false;
- var result = BitmapAStarAlgorithm.Instance.Find(stub, map, start, goal);
- Assert.NotNull(result);
- Assert.NotEmpty(result);
- }
- finally
- {
- ContentFeatureFlags.BitmapPathfindingCache = prevFlag;
- }
+ ContentFeatureFlags.BitmapPathfindingCache = false;
+ var result = BitmapAStarAlgorithm.Instance.Find(stub, map, start, goal);
var statsAfter = StepCache.Instance.GetStats();
stub.Delete();
+ Assert.NotNull(result);
+ Assert.NotEmpty(result);
Assert.Equal(statsBefore.BuildsTotal, statsAfter.BuildsTotal);
}
@@ -346,7 +337,7 @@ public class BitmapAStarAlgorithmTests
StepCache.Instance.MissPromotionThreshold = 2;
var map = Map.Maps[1];
- var stub = new DefaultWalkerStub();
+ var stub = new WalkerStub();
map.GetAverageZ(1500, 1600, out _, out var startZ, out _);
var start = new Point3D(1500, 1600, (sbyte)startZ);
var goal = new Point3D(1498, 1598, (sbyte)startZ);
@@ -376,7 +367,7 @@ public class BitmapAStarAlgorithmTests
StepCache.Instance.MissPromotionWindowMs = 30_000;
var map = Map.Maps[1];
- var stub = new DefaultWalkerStub();
+ var stub = new WalkerStub();
map.GetAverageZ(1500, 1600, out _, out var startZ, out _);
var start = new Point3D(1500, 1600, (sbyte)startZ);
var goal = new Point3D(1498, 1598, (sbyte)startZ);
@@ -401,18 +392,6 @@ public class BitmapAStarAlgorithmTests
);
}
- ///
- /// Plain Mobile — RequiresSlowPath returns false, the bitmap algorithm uses the cache
- /// fast path on every expansion.
- ///
- private sealed class DefaultWalkerStub : Mobile
- {
- public DefaultWalkerStub()
- {
- Body = 0xC9;
- }
- }
-
///
/// Mobile with Player=true and default AccessLevel (Player). Triggers the strict
/// AND-rule for diagonal corner-cut while still using the cache.
@@ -426,20 +405,6 @@ public class BitmapAStarAlgorithmTests
}
}
- ///
- /// BaseCreature with CanSwim=true — uses the cache via capability overlay (walk OR
- /// (wet AND canSwim)). Use the Serial constructor (deserialization path) to bypass
- /// NPCSpeeds init, which requires the npc-speeds.json table loaded — not available
- /// in tests.
- ///
- private sealed class SwimmingStub : BaseCreature
- {
- public SwimmingStub(Serial serial) : base(serial)
- {
- Body = 0xC9;
- }
- }
-
///
/// BaseCreature with CanFly=true — RequiresSlowPath returns true (Z-jumping is beyond
/// the cache's static-only scope), so GetSuccessors short-circuits to the slow path.
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs
index 8ecbd0591..5ecb0cdc1 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs
@@ -1,3 +1,4 @@
+using System;
using Server.Engines.Pathing.Cache;
using Server.Items;
using Server.Mobiles;
@@ -9,25 +10,22 @@ using CalcMoves = Server.Movement.Movement;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class HousePathRoutingTests
+public class HousePathRoutingTests : IDisposable
{
private readonly ITestOutputHelper _output;
+ private readonly StepCacheStateScope _cacheState = new();
public HousePathRoutingTests(ITestOutputHelper output) => _output = output;
- private const int MapId = 1;
+ public void Dispose() => _cacheState.Dispose();
- private sealed class WalkerStub : Mobile
- {
- public WalkerStub() => Body = 0xC9;
- }
+ private const int MapId = 1;
[SkippableFact]
public void PathAround_NeverTraversesAWallCell()
{
TileDataRequirement.SkipIfMissing();
StepCache.Instance.Clear();
- var prevThreshold = StepCache.Instance.MissPromotionThreshold;
StepCache.Instance.MissPromotionThreshold = 1;
var map = Map.Maps[MapId];
Assert.NotNull(map);
@@ -69,7 +67,6 @@ public class HousePathRoutingTests
}
finally
{
- StepCache.Instance.MissPromotionThreshold = prevThreshold;
walker.Delete();
multi.Delete();
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiCacheUsedTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiCacheUsedTests.cs
index fc9e58898..50a27ee44 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiCacheUsedTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiCacheUsedTests.cs
@@ -1,3 +1,4 @@
+using System;
using Server.Engines.Pathing.Cache;
using Server.PathAlgorithms;
using Server.Systems.FeatureFlags;
@@ -6,20 +7,25 @@ using Xunit;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class MultiCacheUsedTests
+public class MultiCacheUsedTests : IDisposable
{
private const int MapId = 1; // Trammel
private const int GuildHouseId = 0x74;
- [Fact]
+ private readonly StepCacheStateScope _cacheState = new();
+
+ public void Dispose() => _cacheState.Dispose();
+
+ [SkippableFact]
public void PathNearMulti_IncrementsMultiLocalHits()
{
+ // Without the client's multi data the house has no footprint for the path to skirt.
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[MapId];
map.GetAverageZ(1480, 1620, out _, out var z, out _);
var houseLoc = new Point3D(1480, 1620, (sbyte)z);
var multi = new TestMulti(GuildHouseId);
- var cacheWas = ContentFeatureFlags.BitmapPathfindingCache;
ContentFeatureFlags.BitmapPathfindingCache = true;
var mover = MultiTestSupport.GetWalkerOracle(map, new Point3D(1480, 1630, (sbyte)z));
@@ -44,7 +50,6 @@ public class MultiCacheUsedTests
{
mover.Delete();
multi.Delete();
- ContentFeatureFlags.BitmapPathfindingCache = cacheWas;
}
}
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs
index 38a6824ea..2b2adf93c 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs
@@ -1,3 +1,4 @@
+using System;
using Server.Engines.Pathing.Cache;
using Server.Items;
using Xunit;
@@ -5,13 +6,17 @@ using Xunit;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class MultiMaskCacheTests
+public class MultiMaskCacheTests : IDisposable
{
private const int MapId = 1;
private const int GuildHouseId = 0x74;
private const int PlaceX = 1480;
private const int PlaceY = 1620;
+ private readonly StepCacheStateScope _cacheState = new();
+
+ public void Dispose() => _cacheState.Dispose();
+
[SkippableFact]
public void TryResolveCoveringMulti_FindsPlacedMulti_AndLocalIndices()
{
@@ -99,7 +104,6 @@ public class MultiMaskCacheTests
map.GetAverageZ(CleanX, CleanY, out _, out var z, out _);
var houseLoc = new Point3D(CleanX, CleanY, (sbyte)z);
var multi = new TestMulti(GuildHouseId);
- var cacheWas = Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache;
Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache = true;
var mover = MultiTestSupport.GetWalkerOracle(map, new Point3D(CleanX, CleanY + 10, (sbyte)z));
try
@@ -124,17 +128,17 @@ public class MultiMaskCacheTests
{
mover.Delete();
multi.Delete();
- Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache = cacheWas;
}
}
- [Fact]
+ [SkippableFact]
public void ClutteredHouse_DoesNotServeCache_ButStillPaths()
{
+ // The clutter is Britain's statics; on a blank map the house has no footprint to dirty.
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[MapId];
map.GetAverageZ(PlaceX, PlaceY, out _, out var z, out _);
var multi = new TestMulti(GuildHouseId);
- var cacheWas = Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache;
Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache = true;
var mover = MultiTestSupport.GetWalkerOracle(map, new Point3D(PlaceX, PlaceY + 10, (sbyte)z));
try
@@ -159,7 +163,6 @@ public class MultiMaskCacheTests
{
mover.Delete();
multi.Delete();
- Server.Systems.FeatureFlags.ContentFeatureFlags.BitmapPathfindingCache = cacheWas;
}
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiPathInvariantTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiPathInvariantTests.cs
index 9a920282f..9ec598916 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiPathInvariantTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiPathInvariantTests.cs
@@ -1,3 +1,4 @@
+using System;
using Server.Engines.Pathing.Cache;
using Server.Items;
using Server.Mobiles;
@@ -9,41 +10,33 @@ using Xunit.Abstractions;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class MultiPathInvariantTests
+public class MultiPathInvariantTests : IDisposable
{
private readonly ITestOutputHelper _output;
+ private readonly StepCacheStateScope _cacheState = new();
public MultiPathInvariantTests(ITestOutputHelper output) => _output = output;
- private const int MapId = 1;
+ public void Dispose() => _cacheState.Dispose();
- private sealed class WalkerStub : Mobile
- {
- public WalkerStub() => Body = 0xC9;
- }
+ private const int MapId = 1;
private static Direction[] FindWithFlag(Mobile m, Map map, Point3D start, Point3D goal, bool cacheOn)
{
- var prev = ContentFeatureFlags.BitmapPathfindingCache;
- try
- {
- ContentFeatureFlags.BitmapPathfindingCache = cacheOn;
- StepCache.Instance.Clear();
- StepCache.Instance.MissPromotionThreshold = 1;
- return BitmapAStarAlgorithm.Instance.Find(m, map, start, goal);
- }
- finally
- {
- ContentFeatureFlags.BitmapPathfindingCache = prev;
- }
+ ContentFeatureFlags.BitmapPathfindingCache = cacheOn;
+ StepCache.Instance.Clear();
+ StepCache.Instance.MissPromotionThreshold = 1;
+ return BitmapAStarAlgorithm.Instance.Find(m, map, start, goal);
}
- [Theory]
+ [SkippableTheory]
// start, goal: straddle a house placed between them (house at ~ midpoint).
[InlineData(1500, 1600, 1500, 1612)] // N-S across the footprint
[InlineData(1494, 1606, 1512, 1606)] // E-W across the footprint
public void Find_CacheOn_EqualsCacheOff_WithHousePresent(int sx, int sy, int gx, int gy)
{
+ // Without the client's multi data the house has no footprint and nothing stands in the way.
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[MapId];
Assert.NotNull(map);
@@ -100,11 +93,11 @@ public class MultiPathInvariantTests
{
var on = FindWithFlag(walker, map, start, goal, cacheOn: true);
var off = FindWithFlag(walker, map, start, goal, cacheOn: false);
- Assert.Equal(off == null, on == null);
- if (on != null)
- {
- Assert.Equal(off, on);
- }
+
+ // Two nulls would agree about nothing: the control route must exist.
+ Assert.NotNull(off);
+ Assert.NotNull(on);
+ Assert.Equal(off, on);
}
finally
{
@@ -112,9 +105,10 @@ public class MultiPathInvariantTests
}
}
- [Fact]
+ [SkippableFact]
public void Find_CacheOn_EqualsCacheOff_RoutesAroundHouse()
{
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[MapId];
Assert.NotNull(map);
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs
index 1103cdb83..8f20d04f4 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs
@@ -1,3 +1,4 @@
+using System;
using Server.Engines.Pathing.Cache;
using Server.Items;
using Xunit;
@@ -6,12 +7,15 @@ using Xunit.Abstractions;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class MultiSplitRoutingTests
+public class MultiSplitRoutingTests : IDisposable
{
private readonly ITestOutputHelper _output;
+ private readonly StepCacheStateScope _cacheState = new();
public MultiSplitRoutingTests(ITestOutputHelper output) => _output = output;
+ public void Dispose() => _cacheState.Dispose();
+
// Open plain on Trammel used by existing pathfinding tests; room for a 7x7 house.
private const int MapId = 1;
private const int PlaceX = 1500;
@@ -60,7 +64,6 @@ public class MultiSplitRoutingTests
public void CleanMap_AfterMultiRemoved_ServesStaticHitAgain()
{
StepCache.Instance.Clear();
- var prevThreshold = StepCache.Instance.MissPromotionThreshold;
StepCache.Instance.MissPromotionThreshold = 1; // build on first touch
var map = Map.Maps[MapId];
Assert.NotNull(map);
@@ -72,20 +75,13 @@ public class MultiSplitRoutingTests
var sector = map.GetRealSector(PlaceX >> 4, PlaceY >> 4);
Assert.True(sector.HasMultis, "placing a multi must set Sector.HasMultis");
- try
- {
- multi.Delete();
- Assert.False(sector.HasMultis, "removing the only multi must clear HasMultis");
+ multi.Delete();
+ Assert.False(sector.HasMultis, "removing the only multi must clear HasMultis");
- // A cell that is NOT a static-fallthrough kind (e.g. off-map / multi) should now be
- // eligible for a real static answer. Use the placement center, which is open plain.
- var mask = StepCache.Instance.TryGetMask(map, PlaceX, PlaceY, (sbyte)z);
- Assert.True(mask.IsHit, $"expected a real static answer after removal, got {mask.HitKind}");
- _output.WriteLine($"post-removal hitKind at center: {mask.HitKind}");
- }
- finally
- {
- StepCache.Instance.MissPromotionThreshold = prevThreshold;
- }
+ // A cell that is NOT a static-fallthrough kind (e.g. off-map / multi) should now be
+ // eligible for a real static answer. Use the placement center, which is open plain.
+ var mask = StepCache.Instance.TryGetMask(map, PlaceX, PlaceY, (sbyte)z);
+ Assert.True(mask.IsHit, $"expected a real static answer after removal, got {mask.HitKind}");
+ _output.WriteLine($"post-removal hitKind at center: {mask.HitKind}");
}
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiTestSupport.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiTestSupport.cs
index 092b28711..0f06a44aa 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiTestSupport.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiTestSupport.cs
@@ -20,12 +20,6 @@ public sealed class TestMulti : BaseMulti
}
}
-/// Default walker body, shared across pathfinding test fixtures.
-public sealed class WalkerStub : Mobile
-{
- public WalkerStub() => Body = 0xC9;
-}
-
///
/// Stand-in for a customizable foundation: Components is a swappable MCL, exactly the
/// runtime-mutation shape HouseFoundation uses (it replaces its MCL wholesale on redesign
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs
index d8d46e7f8..1f993d477 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs
@@ -18,11 +18,6 @@ public class MultiWalkabilityTests
private const int PlaceX = 1500;
private const int PlaceY = 1600;
- private sealed class WalkerStub : Mobile
- {
- public WalkerStub() => Body = 0xC9;
- }
-
[SkippableFact]
public void WallCell_CannotBeEnteredFromAnyAdjacentCell()
{
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs
index 0930bad7a..efb434738 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs
@@ -19,7 +19,7 @@ public class PathfindRecorderTests
try
{
- var stub = new RecorderStub(World.NewMobile);
+ var stub = new BareCreatureStub(World.NewMobile);
stub.DefaultMobileInit();
stub.MoveToWorld(new Point3D(1500, 1600, 0), Map.Maps[1]);
@@ -48,7 +48,7 @@ public class PathfindRecorderTests
{
Assert.True(PathfindRecorder.Enabled);
- var stub = new RecorderStub(World.NewMobile);
+ var stub = new BareCreatureStub(World.NewMobile);
stub.DefaultMobileInit();
stub.MoveToWorld(new Point3D(1500, 1600, 0), Map.Maps[1]);
@@ -89,7 +89,7 @@ public class PathfindRecorderTests
try
{
- var stub = new RecorderStub(World.NewMobile);
+ var stub = new BareCreatureStub(World.NewMobile);
stub.DefaultMobileInit();
stub.MoveToWorld(new Point3D(1500, 1600, 0), Map.Maps[1]);
stub.CanSwim = true;
@@ -100,13 +100,17 @@ public class PathfindRecorderTests
);
PathfindRecorder.SetEnabled(false);
+
+ // BaseCreature's own defaults for these two; the recording must carry whatever they are.
+ var canOpenDoors = JsonBool(stub.CanOpenDoors);
+ var canMoveOverObstacles = JsonBool(stub.CanMoveOverObstacles);
stub.Delete();
var content = File.ReadAllText(path);
Assert.Contains("\"CanSwim\":true", content);
Assert.Contains("\"CanFly\":false", content);
- Assert.Contains("\"CanOpenDoors\":", content); // value depends on BaseCreature defaults
- Assert.Contains("\"CanMoveOverObstacles\":", content); // — same.
+ Assert.Contains($"\"CanOpenDoors\":{canOpenDoors}", content);
+ Assert.Contains($"\"CanMoveOverObstacles\":{canMoveOverObstacles}", content);
}
finally
{
@@ -142,8 +146,5 @@ public class PathfindRecorderTests
}
}
- private sealed class RecorderStub : Server.Mobiles.BaseCreature
- {
- public RecorderStub(Serial serial) : base(serial) => Body = 0xC9;
- }
+ private static string JsonBool(bool value) => value ? "true" : "false";
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/PathingTestSupport.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/PathingTestSupport.cs
index 9027332c7..de0fdaaae 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/PathingTestSupport.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/PathingTestSupport.cs
@@ -4,8 +4,8 @@ using Server.Engines.Pathing.Cache;
namespace Server.Tests.Pathfinding;
///
-/// Shared fixtures for the step-cache tests: the walker the parity tests measure against, the
-/// cell-index arithmetic, and builders for the chunk state several tests inject by hand.
+/// Shared fixtures for the step-cache tests: the test map, the cell-index arithmetic, and builders
+/// for the chunk state several tests inject by hand.
///
internal static class PathingTestSupport
{
@@ -53,17 +53,4 @@ internal static class PathingTestSupport
return data;
}
-
- ///
- /// The default static walker. Deriving straight from rather than
- /// BaseCreature is the point: MovementImpl then sees no creature capabilities (no swim, no fly,
- /// no door-opening), which is exactly the walker the cache bakes for.
- ///
- public sealed class StaticWalker : Mobile
- {
- public StaticWalker()
- {
- Body = 0xC9;
- }
- }
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs
index d29b79ef9..4094c093f 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs
@@ -6,8 +6,12 @@ using Xunit;
namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
-public class StepCacheFileTests
+public class StepCacheFileTests : IDisposable
{
+ private readonly StepCacheStateScope _cacheState = new();
+
+ public void Dispose() => _cacheState.Dispose();
+
///
/// Round-trip a populated cache through a .swb file under lazy loading: build chunks,
/// save, clear (closes lazy readers + drops residents), open as lazy backing store,
@@ -363,23 +367,16 @@ public class StepCacheFileTests
cache.Clear();
cache.PreloadOnLazyOpen = true;
- try
- {
- Assert.True(cache.TryOpenLazyReader(path, map.MapID));
+ Assert.True(cache.TryOpenLazyReader(path, map.MapID));
- // Every chunk should be resident — no further queries needed.
- Assert.Equal(coords.Length, cache.GetStats().ResidentChunks);
- Assert.Equal(0L, cache.GetStats().BuildsTotal); // came from file, not baker
+ // Every chunk should be resident — no further queries needed.
+ Assert.Equal(coords.Length, cache.GetStats().ResidentChunks);
+ Assert.Equal(0L, cache.GetStats().BuildsTotal); // came from file, not baker
- // Subsequent query is a clean Hit, not a Miss_NotBuilt.
- var lookup = cache.TryGetMask(map, coords[0].Item1, coords[0].Item2, sourceZ: 10);
- Assert.Equal(CacheHitKind.Hit, lookup.HitKind);
- Assert.Equal(coords.Length, cache.GetStats().ResidentChunks);
- }
- finally
- {
- cache.PreloadOnLazyOpen = false;
- }
+ // Subsequent query is a clean Hit, not a Miss_NotBuilt.
+ var lookup = cache.TryGetMask(map, coords[0].Item1, coords[0].Item2, sourceZ: 10);
+ Assert.Equal(CacheHitKind.Hit, lookup.HitKind);
+ Assert.Equal(coords.Length, cache.GetStats().ResidentChunks);
}
finally
{
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFingerprintTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFingerprintTests.cs
index dee698e59..6d2bc6f7a 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFingerprintTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFingerprintTests.cs
@@ -15,9 +15,12 @@ public class StepCacheFingerprintTests
/// different one, marking the bake stale and re-baking on every boot. Hashing the file makes
/// the fingerprint a pure function of the client's tile data, immune to those mutations.
///
- [Fact]
+ [SkippableFact]
public void Fingerprint_IgnoresRuntimeTileDataMutation()
{
+ // Without tiledata.mul there is no file to hash, so the fingerprint is constant either way.
+ TileDataRequirement.SkipIfMissing();
+
const int mapId = 1; // Trammel — loaded by the test bootstrap.
var before = StepCacheFile.ComputeFingerprint(mapId);
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs
index 9c410efc7..fd90aedf9 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs
@@ -17,11 +17,9 @@ namespace Server.Tests.Pathfinding;
[Collection("Sequential Pathfinding Tests")]
public class StepCacheLifecycleTests : IDisposable
{
- // The window is a knob on the singleton and Clear() leaves it alone, so a test that narrows
- // it would otherwise leak the narrow window into every later test in the collection.
- private readonly uint _savedWindowMs = StepCache.Instance.MissPromotionWindowMs;
+ private readonly StepCacheStateScope _cacheState = new();
- public void Dispose() => StepCache.Instance.MissPromotionWindowMs = _savedWindowMs;
+ public void Dispose() => _cacheState.Dispose();
/// Resets to a known state and returns the singleton.
private static StepCache FreshCache(int promotionThreshold)
@@ -396,26 +394,19 @@ public class StepCacheLifecycleTests : IDisposable
var cache = FreshCache(promotionThreshold: 1);
cache.MaxResidentChunks = 4;
- try
+ var map = TestMap;
+
+ // Five chunks into a cache that holds four. Which one goes doesn't matter here, only that
+ // the count comes down and the key list stays in step, so equal touch ticks are fine.
+ for (var i = 0; i < 5; i++)
{
- var map = TestMap;
-
- // Five chunks into a cache that holds four.
- for (var i = 0; i < 5; i++)
- {
- cache.TryGetMask(map, PlainX + i * 16, PlainY, sourceZ: 10);
- Thread.Sleep(2); // separate their LastTouchedTicks so LRU has something to order by
- }
-
- cache.EnforceLruCap();
-
- Assert.Equal(4, cache.GetStats().ResidentChunks);
- Assert.True(cache.GetStats().EvictionsByLruCap >= 1L);
- Assert.True(cache.ResidentIndexInSync(), "eviction desynced the key list from the resident set");
- }
- finally
- {
- cache.MaxResidentChunks = 8192;
+ cache.TryGetMask(map, PlainX + i * 16, PlainY, sourceZ: 10);
}
+
+ cache.EnforceLruCap();
+
+ Assert.Equal(4, cache.GetStats().ResidentChunks);
+ Assert.True(cache.GetStats().EvictionsByLruCap >= 1L);
+ Assert.True(cache.ResidentIndexInSync(), "eviction desynced the key list from the resident set");
}
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs
index b38c15277..769945af1 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs
@@ -20,12 +20,15 @@ namespace Server.Tests.Pathfinding;
/// falls through on everything agrees with the slow path perfectly and is worthless.
///
[Collection("Sequential Pathfinding Tests")]
-public class StepCacheParityTests
+public class StepCacheParityTests : IDisposable
{
private readonly ITestOutputHelper _output;
+ private readonly StepCacheStateScope _cacheState = new();
public StepCacheParityTests(ITestOutputHelper output) => _output = output;
+ public void Dispose() => _cacheState.Dispose();
+
// ---- layer 1: the bake agrees with MovementImpl ----
///
@@ -43,7 +46,7 @@ public class StepCacheParityTests
var map = TestMap;
Assert.NotNull(map);
- var walker = new StaticWalker();
+ var walker = new WalkerStub();
walker.MoveToWorld(new Point3D(xStart, yStart, 0), map);
var disagreements = 0;
@@ -154,12 +157,16 @@ public class StepCacheParityTests
/// the land average instead would trip the source-Z guard on raised terrain (a causeway, a
/// walkway) and report a fallthrough that is correct behaviour rather than a fault.
///
- [Theory]
+ [SkippableTheory]
[InlineData("britain_inn_dense", 1480, 1610, 32)]
[InlineData("trammel_open_plain", 1500, 1600, 32)]
[InlineData("britain_causeway", 1475, 1641, 32)]
public void CacheMatchesProbe(string label, int xStart, int yStart, int size)
{
+ // A storage fault (crossed cell index, swapped Z array) only shows against varied terrain;
+ // on a blank map every cell bakes the same answer.
+ TileDataRequirement.SkipIfMissing();
+
var cache = StepCache.Instance;
cache.Clear();
cache.MissPromotionThreshold = 1; // build on first touch: every cell should get a real answer
@@ -253,7 +260,7 @@ public class StepCacheParityTests
/// Cardinals only: the cache stores raw masks and applies the corner-cut at query time, so a
/// raw diagonal bit legitimately differs from MovementImpl's diagonal answer.
///
- [Theory]
+ [SkippableTheory]
// Seeds chosen for the terrain classes the standable-surface bake has to get right. Each one
// floods across a wide local area, so a handful covers thousands of states without a map walk.
[InlineData("brit_sewer_walkway", 6034, 1476, 5, 2500)] // static walkway over impassable land
@@ -262,6 +269,8 @@ public class StepCacheParityTests
[InlineData("trammel_open_plain", 1500, 1600, 10, 2500)] // flat ground: catches clearance false-positives
public void CacheServesReachableWalkStates(string label, int sx, int sy, int sz, int maxStates)
{
+ TileDataRequirement.SkipIfMissing();
+
var cache = StepCache.Instance;
cache.Clear();
cache.MissPromotionThreshold = 1; // build on first touch: every reached state should be answered
@@ -269,7 +278,7 @@ public class StepCacheParityTests
var map = TestMap;
Assert.NotNull(map);
- var walker = new StaticWalker();
+ var walker = new WalkerStub();
walker.MoveToWorld(new Point3D(sx, sy, sz), map);
var startIsWalkable = false;
diff --git a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/AllSpawnFilesLoadTests.cs b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/AllSpawnFilesLoadTests.cs
index 9eb9e1909..946e83e36 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/AllSpawnFilesLoadTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/AllSpawnFilesLoadTests.cs
@@ -20,10 +20,9 @@ public class AllSpawnFilesLoadTests
public void EverySpawnFile_DeserializesAndBuildsSpawners()
{
var root = Path.Combine(Core.BaseDirectory, "Data", "Spawns");
- if (!Directory.Exists(root))
- {
- return; // distribution data not present in this checkout
- }
+ // Data/ is copied into the test output by the build; a missing folder means the build was run
+ // without SolutionDir, and passing would hide that nothing was checked.
+ Assert.True(Directory.Exists(root), $"Spawn data not found at {root}");
var failures = new List();
var fileCount = 0;
@@ -64,6 +63,7 @@ public class AllSpawnFilesLoadTests
}
}
+ Assert.NotEqual(0, fileCount);
Assert.True(
failures.Count == 0,
$"Loaded {fileCount} files, built {spawnerCount} spawners. Failures:\n{string.Join("\n", failures)}"
diff --git a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/ImportCleanupTests.cs b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/ImportCleanupTests.cs
index 6b4bba2dd..e30cc289d 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/ImportCleanupTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/ImportCleanupTests.cs
@@ -3,22 +3,24 @@ using System.Collections.Generic;
using System.IO;
using Server;
using Server.Engines.Spawners;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Engines.Spawners.Json;
[Collection("Sequential UOContent Tests")]
-public class ImportCleanupTests
+public class ImportCleanupTests : IDisposable
{
private const string SpawnerGuid = "11111111-1111-1111-1111-111111111111";
+ private readonly TempDirectory _dir = new("muo-spawner-import");
+
+ public void Dispose() => _dir.Dispose();
+
[Fact]
public void Import_ValidFile_PlacesSpawner()
{
- var dir = Path.Combine(Path.GetTempPath(), "muo-spawner-import-" + Guid.NewGuid().ToString("N"));
- Directory.CreateDirectory(dir);
- var path = Path.Combine(dir, "test.json");
- File.WriteAllText(path, """
+ var path = _dir.WriteFile("test.json", """
[
{
"$type": "Spawner",
@@ -53,22 +55,15 @@ public class ImportCleanupTests
finally
{
placedSpawner?.Delete();
- if (Directory.Exists(dir))
- {
- Directory.Delete(dir, true);
- }
}
}
+ // A DTO with no map entry deserializes with map == null. The importer rejects it before building
+ // a spawner, so nothing is registered and nothing lands on any live map.
[Fact]
- public void Import_NoMap_DeletesOrphanedSpawner()
+ public void Import_NoMap_SkipsTheSpawner()
{
- // A spawner with no map entry deserialized by STJ will have map == null (or Map.Internal).
- // Verify it does NOT end up on any live map.
- var dir = Path.Combine(Path.GetTempPath(), "muo-spawner-import-nomap-" + Guid.NewGuid().ToString("N"));
- Directory.CreateDirectory(dir);
- var path = Path.Combine(dir, "test-nomap.json");
- File.WriteAllText(path, """
+ var path = _dir.WriteFile("test-nomap.json", """
[
{
"$type": "Spawner",
@@ -81,45 +76,28 @@ public class ImportCleanupTests
]
""");
- try
- {
- var all = new Dictionary();
- ImportSpawnersCommand.ImportFile(new FileInfo(path), all);
+ var guid = new Guid("22222222-2222-2222-2222-222222222222");
+ var all = new Dictionary();
+ ImportSpawnersCommand.ImportFile(new FileInfo(path), all);
- // The spawner must NOT be in allSpawners (rejected due to missing map).
- Assert.DoesNotContain(new Guid("22222222-2222-2222-2222-222222222222"), all.Keys);
- }
- finally
+ Assert.DoesNotContain(guid, all.Keys);
+
+ var maps = Map.AllMaps;
+ for (var i = 0; i < maps.Count; i++)
{
- if (Directory.Exists(dir))
+ foreach (var spawner in maps[i].GetItemsAt(new Point3D(400, 400, 0)))
{
- Directory.Delete(dir, true);
+ Assert.NotEqual(guid, spawner.Guid);
}
}
}
- private static int CountSpawnersNear(Point3D p)
- {
- var n = 0;
- foreach (var _ in Map.Felucca.GetItemsAt(p))
- {
- n++;
- }
-
- return n;
- }
-
[Fact]
public void Import_DuplicateLocation_ReplacesExistingSpawner()
{
// Verifies that an existing spawner at the same location and type is removed and replaced.
- var dir = Path.Combine(Path.GetTempPath(), "muo-spawner-import-dup-" + Guid.NewGuid().ToString("N"));
- Directory.CreateDirectory(dir);
- var path = Path.Combine(dir, "test-dup.json");
-
var newGuid = new Guid("33333333-3333-3333-3333-333333333333");
- // Use a non-interpolated raw string literal; the guid is a known constant so hardcode it.
- File.WriteAllText(path, """
+ var path = _dir.WriteFile("test-dup.json", """
[
{
"$type": "Spawner",
@@ -133,13 +111,13 @@ public class ImportCleanupTests
]
""");
- // Place a pre-existing spawner at the same location.
var existing = new Spawner(1, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10));
- existing.MoveToWorld(new Point3D(310, 310, 0), Map.Felucca);
-
BaseSpawner placedSpawner = null;
+
try
{
+ existing.MoveToWorld(new Point3D(310, 310, 0), Map.Felucca);
+
var all = new Dictionary { [existing.Guid] = existing };
ImportSpawnersCommand.ImportFile(new FileInfo(path), all);
@@ -160,15 +138,8 @@ public class ImportCleanupTests
}
finally
{
- if (!existing.Deleted)
- {
- existing.Delete();
- }
+ existing.Delete();
placedSpawner?.Delete();
- if (Directory.Exists(dir))
- {
- Directory.Delete(dir, true);
- }
}
}
}
diff --git a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/SpawnerDtoEntryTests.cs b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/SpawnerDtoEntryTests.cs
index 8a25c4b0f..930f059ce 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/SpawnerDtoEntryTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Spawners/Json/SpawnerDtoEntryTests.cs
@@ -15,15 +15,21 @@ public class SpawnerDtoEntryTests
public void Export_WritesEntriesAtSameJsonPosition_WithDisabledOnlyWhenSet()
{
var spawner = new Spawner(1, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10), 0, default, "Rabbit", "Bird");
- spawner.Entries[1].Disabled = true;
- var json = SpawnerJsonSerializer.SerializeCompact(new List { spawner.ToDto() });
+ try
+ {
+ spawner.Entries[1].Disabled = true;
- Assert.Contains("\"entries\": [\n { \"name\": \"Rabbit\"", json);
- Assert.Contains("{ \"name\": \"Bird\"", json);
- Assert.Equal(1, CountOccurrences(json, "\"disabled\": true"));
+ var json = SpawnerJsonSerializer.SerializeCompact(new List { spawner.ToDto() });
- spawner.Delete();
+ Assert.Contains("\"entries\": [\n { \"name\": \"Rabbit\"", json);
+ Assert.Contains("{ \"name\": \"Bird\"", json);
+ Assert.Equal(1, CountOccurrences(json, "\"disabled\": true"));
+ }
+ finally
+ {
+ spawner.Delete();
+ }
}
[Fact]
@@ -35,11 +41,16 @@ public class SpawnerDtoEntryTests
var dtos = JsonSerializer.Deserialize>(json, SpawnerJsonSerializer.Options);
var spawner = dtos![0].ToSpawner();
- Assert.Single(spawner.Entries);
- Assert.True(spawner.Entries[0].Disabled);
- Assert.Same(dtos[0].EntryView[0], spawner.Entries[0]);
-
- spawner.Delete();
+ try
+ {
+ Assert.Single(spawner.Entries);
+ Assert.True(spawner.Entries[0].Disabled);
+ Assert.Same(dtos[0].EntryView[0], spawner.Entries[0]);
+ }
+ finally
+ {
+ spawner.Delete();
+ }
}
private static int CountOccurrences(string source, string value)
diff --git a/Projects/UOContent.Tests/Tests/Engines/Spawners/SectorSpawnCacheTests.cs b/Projects/UOContent.Tests/Tests/Engines/Spawners/SectorSpawnCacheTests.cs
index 1acc29960..ad67e96b9 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Spawners/SectorSpawnCacheTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Spawners/SectorSpawnCacheTests.cs
@@ -260,14 +260,8 @@ public class SectorSpawnCacheManagerTests
}
}
-[Collection("SectorSpawnCache")]
public class SpiralScanTests
{
- public SpiralScanTests()
- {
- SectorSpawnCacheManager.ClearAll();
- }
-
[Theory]
[InlineData(1, 0, -1, -1)] // Ring 1, position 0: top-left
[InlineData(1, 1, 0, -1)] // Ring 1, position 1: top
diff --git a/Projects/UOContent.Tests/Tests/Engines/Spawners/SpawnerEntryOwnershipTests.cs b/Projects/UOContent.Tests/Tests/Engines/Spawners/SpawnerEntryOwnershipTests.cs
index 96023870d..6d9e8e711 100644
--- a/Projects/UOContent.Tests/Tests/Engines/Spawners/SpawnerEntryOwnershipTests.cs
+++ b/Projects/UOContent.Tests/Tests/Engines/Spawners/SpawnerEntryOwnershipTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
@@ -11,26 +12,43 @@ using Xunit;
namespace UOContent.Tests.Engines.Spawners;
[Collection("Sequential UOContent Tests")]
-public class SpawnerEntryOwnershipTests
+public class SpawnerEntryOwnershipTests : IDisposable
{
+ // Deleted in Dispose so a failed assertion cannot leave spawners registered in the world.
+ private readonly List- _created = [];
+
+ public void Dispose()
+ {
+ for (var i = 0; i < _created.Count; i++)
+ {
+ _created[i].Delete();
+ }
+ }
+
+ private T Track(T item) where T : Item
+ {
+ _created.Add(item);
+ return item;
+ }
+
+ private Spawner NewSpawner(params string[] names) =>
+ Track(new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, names));
+
[Fact]
public void Disabled_DefaultsFalse_AndRoundTripsBinary()
{
- var spawner = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit", "Bird");
+ var spawner = NewSpawner("Rabbit", "Bird");
Assert.False(spawner.Entries[0].Disabled);
Assert.True(spawner.Entries[0].Enabled);
spawner.Entries[1].Disabled = true;
var bytes = SpawnerBlob.Write(spawner);
- var loaded = SpawnerBlob.Read(bytes, (Serial)0x40001234);
+ var loaded = Track(SpawnerBlob.Read(bytes, (Serial)0x40001234));
Assert.Equal(2, loaded.Entries.Count);
Assert.False(loaded.Entries[0].Disabled);
Assert.True(loaded.Entries[1].Disabled);
-
- spawner.Delete();
- loaded.Delete();
}
[Fact]
@@ -48,7 +66,7 @@ public class SpawnerEntryOwnershipTests
[Fact]
public void AddRemoveClear_OperateOnOwnerList()
{
- var spawner = new Spawner();
+ var spawner = Track(new Spawner());
Assert.Empty(spawner.Entries);
var a = spawner.AddEntry("Rabbit", 100, 2, false);
@@ -63,17 +81,15 @@ public class SpawnerEntryOwnershipTests
spawner.RemoveAllEntries();
Assert.Empty(spawner.Entries);
-
- spawner.Delete();
}
[Fact]
public void Dupe_ClonesEntriesIntoIndependentList()
{
- var source = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit", "Bird");
+ var source = NewSpawner("Rabbit", "Bird");
source.Entries[1].Disabled = true;
- var copy = new Spawner();
+ var copy = Track(new Spawner());
source.Dupe(copy);
Assert.Equal(2, copy.Entries.Count);
@@ -84,31 +100,25 @@ public class SpawnerEntryOwnershipTests
source.AddEntry("Orc", 100, 1, false);
Assert.Equal(2, copy.Entries.Count);
-
- source.Delete();
- copy.Delete();
}
[Fact]
public void CopyEntriesTo_ReplacesTargetEntries()
{
- var source = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit");
- var target = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Orc", "Troll");
+ var source = NewSpawner("Rabbit");
+ var target = NewSpawner("Orc", "Troll");
source.CopyEntriesTo(target);
Assert.Single(target.Entries);
Assert.Equal("Rabbit", target.Entries[0].SpawnedName);
Assert.NotSame(source.Entries[0], target.Entries[0]);
-
- source.Delete();
- target.Delete();
}
[Fact]
public void CopyEntriesTo_Self_IsNoOp()
{
- var spawner = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit", "Bird");
+ var spawner = NewSpawner("Rabbit", "Bird");
var first = spawner.Entries[0];
var second = spawner.Entries[1];
@@ -117,18 +127,16 @@ public class SpawnerEntryOwnershipTests
Assert.Equal(2, spawner.Entries.Count);
Assert.Same(first, spawner.Entries[0]);
Assert.Same(second, spawner.Entries[1]);
-
- spawner.Delete();
}
[Fact]
public void RemoveEntry_ForeignEntry_IsIgnored()
{
- var a = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Rabbit");
- var b = new Spawner(1, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2), 0, default, "Bird");
+ var a = NewSpawner("Rabbit");
+ var b = NewSpawner("Bird");
var foreign = b.Entries[0];
- var spawned = new Item(0x1f13);
+ var spawned = Track(new Item(0x1f13));
foreign.AddToSpawned(spawned);
a.RemoveEntry(foreign);
@@ -140,10 +148,6 @@ public class SpawnerEntryOwnershipTests
Assert.False(spawned.Deleted);
Assert.Single(foreign.Spawned);
-
- spawned.Delete();
- a.Delete();
- b.Delete();
}
// Manual: remove the Skip, run, and read the numbers from the thrown assertion.
diff --git a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseChildPublicTests.cs b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseChildPublicTests.cs
index 01a58a697..ca3c7399f 100644
--- a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseChildPublicTests.cs
+++ b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseChildPublicTests.cs
@@ -17,30 +17,42 @@ public class CorpseChildPublicTests
private static VikingSword EquipSword(Mobile owner)
{
var sword = new VikingSword { Layer = Layer.OneHanded };
- Assert.True(owner.EquipItem(sword));
+ var equipped = owner.EquipItem(sword);
+ if (!equipped)
+ {
+ sword.Delete();
+ }
+
+ Assert.True(equipped);
return sword;
}
+ private static Mobile NewOwner(int body)
+ {
+ var owner = new Mobile(World.NewMobile);
+ owner.DefaultMobileInit();
+ owner.Body = body;
+ return owner;
+ }
+
[Fact]
public void IsChildPublic_HumanCorpseWithWornGear_ReturnsTrue()
{
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
-
- var wornItem = EquipSword(owner);
-
- var corpse = new Corpse(owner, owner.Items);
+ var owner = NewOwner(0x190);
+ Corpse corpse = null;
try
{
+ var wornItem = EquipSword(owner);
+ corpse = new Corpse(owner, owner.Items);
+
Assert.Equal(0x2006, corpse.ItemID);
Assert.True(((Body)corpse.Amount).IsHuman);
Assert.True(corpse.IsChildPublic(wornItem));
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
}
}
@@ -48,26 +60,24 @@ public class CorpseChildPublicTests
[Fact]
public void IsChildPublic_HumanCorpseWithLootItem_ReturnsFalse()
{
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
-
- var wornItem = EquipSword(owner);
-
- var corpse = new Corpse(owner, owner.Items);
-
- var lootItem = new Gold(100);
- corpse.DropItem(lootItem);
+ var owner = NewOwner(0x190);
+ Corpse corpse = null;
try
{
+ EquipSword(owner);
+ corpse = new Corpse(owner, owner.Items);
+
+ var lootItem = new Gold(100);
+ corpse.DropItem(lootItem);
+
Assert.Equal(0x2006, corpse.ItemID);
Assert.True(((Body)corpse.Amount).IsHuman);
Assert.False(corpse.IsChildPublic(lootItem));
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
}
}
@@ -75,16 +85,14 @@ public class CorpseChildPublicTests
[Fact]
public void IsChildPublic_BoneCorpse_ReturnsFalse()
{
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
-
- var wornItem = EquipSword(owner);
-
- var corpse = new Corpse(owner, owner.Items);
+ var owner = NewOwner(0x190);
+ Corpse corpse = null;
try
{
+ var wornItem = EquipSword(owner);
+ corpse = new Corpse(owner, owner.Items);
+
corpse.ItemID = 0xECA;
Assert.True(((Body)corpse.Amount).IsHuman);
@@ -93,7 +101,7 @@ public class CorpseChildPublicTests
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
}
}
@@ -101,23 +109,21 @@ public class CorpseChildPublicTests
[Fact]
public void IsChildPublic_NonHumanCorpse_ReturnsFalse()
{
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0xC9;
-
- var wornItem = EquipSword(owner);
-
- var corpse = new Corpse(owner, owner.Items);
+ var owner = NewOwner(0xC9);
+ Corpse corpse = null;
try
{
+ var wornItem = EquipSword(owner);
+ corpse = new Corpse(owner, owner.Items);
+
Assert.Equal(0x2006, corpse.ItemID);
Assert.False(((Body)corpse.Amount).IsHuman);
Assert.False(corpse.IsChildPublic(wornItem));
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
}
}
@@ -129,36 +135,48 @@ public class CorpseChildPublicTests
return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
}
+ private static Mobile NewBystander(NetState ns, Point3D location)
+ {
+ ns.Account = new MockAccount();
+ var bystander = new Mobile(World.NewMobile);
+ bystander.DefaultMobileInit();
+ ns.Mobile = bystander;
+ bystander.NetState = ns;
+ bystander.MoveToWorld(location, Map.Felucca);
+ return bystander;
+ }
+
+ private static void DeleteBystander(NetState ns, Mobile bystander)
+ {
+ ns.Mobile = null;
+ ns.Dispose();
+ bystander?.Delete();
+ }
+
[Fact]
public void HumanCorpse_WornGearRemovalBroadcasts_LootRemovalDoesNot()
{
var corpseLoc = new Point3D(1500, 1500, 0);
var bystanderNs = PacketTestUtilities.CreateTestNetState();
- bystanderNs.Account = new MockAccount();
- var bystander = new Mobile(World.NewMobile);
- bystander.DefaultMobileInit();
- bystanderNs.Mobile = bystander;
- bystander.NetState = bystanderNs;
- bystander.MoveToWorld(new Point3D(corpseLoc.X + 1, corpseLoc.Y, corpseLoc.Z), Map.Felucca);
-
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
-
- var sword = new VikingSword();
- var corpse = new Corpse(owner, [sword]);
- corpse.AddItem(sword);
-
- var lootItem = new Gold(100);
- corpse.DropItem(lootItem);
-
- corpse.MoveToWorld(corpseLoc, Map.Felucca);
-
+ Mobile bystander = null;
+ var owner = NewOwner(0x190);
+ Corpse corpse = null;
Container pouch = null;
try
{
+ bystander = NewBystander(bystanderNs, new Point3D(corpseLoc.X + 1, corpseLoc.Y, corpseLoc.Z));
+
+ var sword = new VikingSword();
+ corpse = new Corpse(owner, [sword]);
+ corpse.AddItem(sword);
+
+ var lootItem = new Gold(100);
+ corpse.DropItem(lootItem);
+
+ corpse.MoveToWorld(corpseLoc, Map.Felucca);
+
// A container not in the world is a valid destination; only the removal from the corpse matters here.
pouch = new Container(0xE75);
pouch.AddItem(sword);
@@ -171,11 +189,9 @@ public class CorpseChildPublicTests
finally
{
pouch?.Delete();
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
- bystanderNs.Mobile = null;
- bystanderNs.Dispose();
- bystander.Delete();
+ DeleteBystander(bystanderNs, bystander);
}
}
@@ -185,25 +201,20 @@ public class CorpseChildPublicTests
var corpseLoc = new Point3D(1500, 1500, 0);
var bystanderNs = PacketTestUtilities.CreateTestNetState();
- bystanderNs.Account = new MockAccount();
- var bystander = new Mobile(World.NewMobile);
- bystander.DefaultMobileInit();
- bystanderNs.Mobile = bystander;
- bystander.NetState = bystanderNs;
- bystander.MoveToWorld(new Point3D(corpseLoc.X + 1, corpseLoc.Y, corpseLoc.Z), Map.Felucca);
-
- var owner = new Mobile(World.NewMobile);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
- owner.MoveToWorld(new Point3D(corpseLoc.X - 1, corpseLoc.Y, corpseLoc.Z), Map.Felucca);
-
+ Mobile bystander = null;
+ var owner = NewOwner(0x190);
var sword = new VikingSword { Layer = Layer.OneHanded };
- var corpse = new Corpse(owner, [sword]);
- corpse.AddItem(sword);
- corpse.MoveToWorld(corpseLoc, Map.Felucca);
+ Corpse corpse = null;
try
{
+ bystander = NewBystander(bystanderNs, new Point3D(corpseLoc.X + 1, corpseLoc.Y, corpseLoc.Z));
+ owner.MoveToWorld(new Point3D(corpseLoc.X - 1, corpseLoc.Y, corpseLoc.Z), Map.Felucca);
+
+ corpse = new Corpse(owner, [sword]);
+ corpse.AddItem(sword);
+ corpse.MoveToWorld(corpseLoc, Map.Felucca);
+
// Lifting takes the piece off the worn list before the remove is sent; the remove must
// still reach everyone who was shown it worn.
owner.Lift(sword, sword.Amount, out var rejected, out _);
@@ -216,11 +227,9 @@ public class CorpseChildPublicTests
{
owner.Holding = null;
sword.Delete();
- corpse.Delete();
+ corpse?.Delete();
owner.Delete();
- bystanderNs.Mobile = null;
- bystanderNs.Dispose();
- bystander.Delete();
+ DeleteBystander(bystanderNs, bystander);
}
}
}
diff --git a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseLootTests.cs b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseLootTests.cs
index 26556155e..9cfec050a 100644
--- a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseLootTests.cs
+++ b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpseLootTests.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using Server;
using Server.Items;
using Xunit;
@@ -11,19 +10,22 @@ public class CorpseLootTests
[Fact]
public void LiftedWornGear_PutBack_StaysLoot()
{
- var owner = new Mobile((Serial)0x1);
- owner.DefaultMobileInit();
- owner.Body = 0x190;
-
- // Weapons take their layer from tiledata, which CI doesn't load.
- var sword = new VikingSword { Layer = Layer.OneHanded };
- Assert.True(owner.EquipItem(sword));
-
- var corpse = new Corpse(owner, new List
- { sword });
- corpse.DropItem(sword);
+ var owner = new Mobile(World.NewMobile);
+ VikingSword sword = null;
+ Corpse corpse = null;
try
{
+ owner.DefaultMobileInit();
+ owner.Body = 0x190;
+
+ // Weapons take their layer from tiledata, which CI doesn't load.
+ sword = new VikingSword { Layer = Layer.OneHanded };
+ Assert.True(owner.EquipItem(sword));
+
+ corpse = new Corpse(owner, [sword]);
+ corpse.DropItem(sword);
+
Assert.Contains(sword, corpse.EquipItems);
corpse.OnItemLifted(owner, sword);
@@ -35,7 +37,8 @@ public class CorpseLootTests
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
+ sword?.Delete();
owner.Delete();
}
}
diff --git a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs
index b5cec8aa6..1669147f2 100644
--- a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs
+++ b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs
@@ -10,19 +10,34 @@ namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class CorpsePacketTests
{
+ // Weapons take their layer from tiledata, which CI doesn't load; without an explicit layer the
+ // equip is refused and the packets would only cover an empty item list.
+ private static VikingSword EquipSword(Mobile m)
+ {
+ var sword = new VikingSword { Layer = Layer.OneHanded };
+ var equipped = m.EquipItem(sword);
+ if (!equipped)
+ {
+ sword.Delete();
+ }
+
+ Assert.True(equipped);
+ return sword;
+ }
+
[Fact]
public void TestCorpseEquipPacket()
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
-
- var weapon = new VikingSword();
- m.EquipItem(weapon);
-
- var c = new Corpse(m, m.Items);
+ var m = new Mobile(World.NewMobile);
+ VikingSword weapon = null;
+ Corpse c = null;
try
{
+ m.DefaultMobileInit();
+ weapon = EquipSword(m);
+ c = new Corpse(m, m.Items);
+
var expected = new CorpseEquip(m, c).Compile();
using var ns = PacketTestUtilities.CreateTestNetState();
@@ -33,8 +48,9 @@ public class CorpsePacketTests
}
finally
{
- c.Delete();
- weapon.Delete();
+ c?.Delete();
+ weapon?.Delete();
+ m.Delete();
}
}
@@ -43,16 +59,16 @@ public class CorpsePacketTests
[InlineData(ProtocolChanges.ContainerGridLines)]
public void TestCorpseContainerPacket(ProtocolChanges changes)
{
- var m = new Mobile((Serial)0x1);
- m.DefaultMobileInit();
-
- var weapon = new VikingSword();
- m.EquipItem(weapon);
-
- var c = new Corpse(m, m.Items);
+ var m = new Mobile(World.NewMobile);
+ VikingSword weapon = null;
+ Corpse c = null;
try
{
+ m.DefaultMobileInit();
+ weapon = EquipSword(m);
+ c = new Corpse(m, m.Items);
+
using var ns = PacketTestUtilities.CreateTestNetState();
ns.ProtocolChanges = changes;
@@ -65,8 +81,9 @@ public class CorpsePacketTests
}
finally
{
- c.Delete();
- weapon.Delete();
+ c?.Delete();
+ weapon?.Delete();
+ m.Delete();
}
}
}
diff --git a/Projects/UOContent.Tests/Tests/Items/VendorBuybackPackTests.cs b/Projects/UOContent.Tests/Tests/Items/VendorBuybackPackTests.cs
index cc0ccad42..b9f13a2f1 100644
--- a/Projects/UOContent.Tests/Tests/Items/VendorBuybackPackTests.cs
+++ b/Projects/UOContent.Tests/Tests/Items/VendorBuybackPackTests.cs
@@ -87,14 +87,14 @@ public class VendorBuybackPackTests
var pack = new VendorBuybackPack();
var items = new Item[10];
- for (var i = 0; i < items.Length; i++)
- {
- items[i] = new Item(0x1234);
- pack.AddBuyback(items[i], 250);
- }
-
try
{
+ for (var i = 0; i < items.Length; i++)
+ {
+ items[i] = new Item(0x1234);
+ pack.AddBuyback(items[i], 250);
+ }
+
pack.Purge();
Assert.Empty(pack.Items);
diff --git a/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs b/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs
index da896ba8b..745922d08 100644
--- a/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs
+++ b/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs
@@ -10,13 +10,19 @@ namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class ThrowingTests
{
- // Exposes protected ModifyHitChance so the math can be verified directly.
+ // Exposes protected ModifyHitChance so the math can be verified directly. The requirement and
+ // throw band are pinned here so retuning a shipped weapon cannot move these expectations.
private class TestThrown : BaseThrown
{
- public TestThrown() : base(0x8FF) { }
+ // Weapons take their layer from tiledata, which CI doesn't load; without it AddItem is refused.
+ public TestThrown() : base(0x8FF) => Layer = Layer.OneHanded;
+
+ public override int AosStrengthReq => 60;
public override int MinThrowRange => 4;
+ public override int MaxThrowRange => 7;
+
public double TestModifyHitChance(Mobile attacker, Mobile defender, double chance) =>
ModifyHitChance(attacker, defender, chance);
@@ -220,18 +226,19 @@ public class ThrowingTests
}
}
- // DefMaxRange STR scaling (uses SoulGlaive: StrReq 60, Min 8, Max 11)
+ // DefMaxRange STR scaling (TestThrown: StrReq 60, Min 4, Max 7, so one tile per 26 Str)
[Fact]
public void DefMaxRange_AtStrReq_EqualsMinThrowRange()
{
- var weapon = new SoulGlaive();
+ var weapon = new TestThrown();
var attacker = CreateMobile(Map.Felucca, new Point3D(5800, 500, 0));
try
{
attacker.RawStr = 60; // == AosStrengthReq
attacker.AddItem(weapon);
- Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 8
+ Assert.Same(attacker, weapon.Parent);
+ Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 4
}
finally
{
@@ -243,13 +250,14 @@ public class ThrowingTests
[Fact]
public void DefMaxRange_At140Str_EqualsMaxThrowRange()
{
- var weapon = new SoulGlaive();
+ var weapon = new TestThrown();
var attacker = CreateMobile(Map.Felucca, new Point3D(5810, 500, 0));
try
{
attacker.RawStr = 140;
attacker.AddItem(weapon);
- Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 11
+ Assert.Same(attacker, weapon.Parent);
+ Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 7
}
finally
{
@@ -261,13 +269,14 @@ public class ThrowingTests
[Fact]
public void DefMaxRange_AboveMaxStr_IsCappedAtMaxThrowRange()
{
- var weapon = new SoulGlaive();
+ var weapon = new TestThrown();
var attacker = CreateMobile(Map.Felucca, new Point3D(5820, 500, 0));
try
{
- attacker.RawStr = 200; // uncapped formula would give 13
+ attacker.RawStr = 200; // uncapped formula would give 9
attacker.AddItem(weapon);
- Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 11, not 13
+ Assert.Same(attacker, weapon.Parent);
+ Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 7, not 9
}
finally
{
@@ -279,13 +288,14 @@ public class ThrowingTests
[Fact]
public void DefMaxRange_BelowStrReq_FlooredAtMinThrowRange()
{
- var weapon = new SoulGlaive();
+ var weapon = new TestThrown();
var attacker = CreateMobile(Map.Felucca, new Point3D(5830, 500, 0));
try
{
attacker.RawStr = 10; // below StrReq 60
attacker.AddItem(weapon);
- Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 8, not 6
+ Assert.Same(attacker, weapon.Parent);
+ Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 4, not 3
}
finally
{
@@ -305,6 +315,7 @@ public class ThrowingTests
{
attacker.RawStr = 140; // MaxRange == MaxThrowRange == 7
attacker.AddItem(weapon);
+ Assert.Same(attacker, weapon.Parent);
var defender = CreateMobile(map, new Point3D(5907, 500, 0)); // distance 7 == MaxRange
try
{
@@ -333,6 +344,7 @@ public class ThrowingTests
{
attacker.RawStr = 140; // MaxRange 7
attacker.AddItem(weapon);
+ Assert.Same(attacker, weapon.Parent);
var defender = CreateMobile(map, new Point3D(5925, 500, 0)); // distance 5 < 7
try
{
diff --git a/Projects/UOContent.Tests/Tests/Misc/CorpseNotorietyTests.cs b/Projects/UOContent.Tests/Tests/Misc/CorpseNotorietyTests.cs
index a46c56003..68be84e7b 100644
--- a/Projects/UOContent.Tests/Tests/Misc/CorpseNotorietyTests.cs
+++ b/Projects/UOContent.Tests/Tests/Misc/CorpseNotorietyTests.cs
@@ -17,22 +17,23 @@ public class CorpseNotorietyTests
public void PlayerCorpseNotorietyIgnoresTransformedBody(int bodyMod, int expected, int kills = 0)
{
var victim = new PlayerMobile(World.NewMobile);
- victim.DefaultMobileInit();
- victim.Kills = kills;
- victim.BodyMod = bodyMod;
-
var looter = new PlayerMobile(World.NewMobile);
- looter.DefaultMobileInit();
-
- var corpse = new Corpse(victim, []);
+ Corpse corpse = null;
try
{
+ victim.DefaultMobileInit();
+ victim.Kills = kills;
+ victim.BodyMod = bodyMod;
+ looter.DefaultMobileInit();
+
+ corpse = new Corpse(victim, []);
+
Assert.Equal(expected, NotorietyHandlers.CorpseNotoriety(looter, corpse));
}
finally
{
- corpse.Delete();
+ corpse?.Delete();
victim.Delete();
looter.Delete();
}
diff --git a/Projects/UOContent.Tests/Tests/Misc/EmailTests.cs b/Projects/UOContent.Tests/Tests/Misc/EmailTests.cs
index de6825c9f..06d940512 100644
--- a/Projects/UOContent.Tests/Tests/Misc/EmailTests.cs
+++ b/Projects/UOContent.Tests/Tests/Misc/EmailTests.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Reflection;
using Server.Configurations;
using Server.Misc;
using Xunit;
@@ -7,13 +8,22 @@ using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
-public class EmailTests
+public class EmailTests : IDisposable
{
+ private static readonly PropertyInfo _emailEnabled =
+ typeof(EmailConfiguration).GetProperty(nameof(EmailConfiguration.EmailEnabled));
+
+ private readonly bool _savedEmailEnabled = EmailConfiguration.EmailEnabled;
+
+ // Pinned off rather than read from the host's email.json, so a developer config with email
+ // enabled cannot turn these into real sends.
+ public EmailTests() => _emailEnabled.SetValue(null, false);
+
+ public void Dispose() => _emailEnabled.SetValue(null, _savedEmailEnabled);
+
[Fact]
public void SendCrashEmailDoesNothingWhileEmailIsDisabled()
{
- Assert.False(EmailConfiguration.EmailEnabled, "The test host never enables email");
-
// Missing input must be ignored while email is disabled.
var report = Path.Combine(Path.GetTempPath(), $"crash-email-test-{Guid.NewGuid():N}", "Crash.log");
@@ -23,8 +33,7 @@ public class EmailTests
[Fact]
public void SendQueueEmailDoesNothingWhileEmailIsDisabled()
{
- Assert.False(EmailConfiguration.EmailEnabled, "The test host never enables email");
-
+ // A null entry would throw on the enabled path, so returning cleanly shows the guard held.
Email.SendQueueEmail(null, "Other");
}
diff --git a/Projects/UOContent.Tests/Tests/Misc/GuildSerializePurityTests.cs b/Projects/UOContent.Tests/Tests/Misc/GuildSerializePurityTests.cs
index f596282f6..86787e0f2 100644
--- a/Projects/UOContent.Tests/Tests/Misc/GuildSerializePurityTests.cs
+++ b/Projects/UOContent.Tests/Tests/Misc/GuildSerializePurityTests.cs
@@ -22,40 +22,55 @@ public class GuildSerializePurityTests
public void SerializeDoesNotRecalculateFealtyOrMutateState()
{
var leader = CreatePlayer();
- var guild = new Guild(leader, "Purity Test Guild", "PTG");
- var staleFealty = Core.Now - TimeSpan.FromDays(3);
- guild.LastFealty = staleFealty;
+ Guild guild = null;
- var writer = new BufferWriter(true);
- guild.Serialize(writer);
+ try
+ {
+ guild = new Guild(leader, "Purity Test Guild", "PTG");
+ var staleFealty = Core.Now - TimeSpan.FromDays(3);
+ guild.LastFealty = staleFealty;
- Assert.Equal(staleFealty, guild.LastFealty);
- Assert.Same(leader, guild.Leader);
+ var writer = new BufferWriter(true);
+ guild.Serialize(writer);
- var first = writer.Buffer.AsSpan(0, (int)writer.Position).ToArray();
- writer.Seek(0, SeekOrigin.Begin);
- guild.Serialize(writer);
- var second = writer.Buffer.AsSpan(0, (int)writer.Position).ToArray();
+ Assert.Equal(staleFealty, guild.LastFealty);
+ Assert.Same(leader, guild.Leader);
- Assert.Equal(first, second);
+ var first = writer.Buffer.AsSpan(0, (int)writer.Position).ToArray();
+ writer.Seek(0, SeekOrigin.Begin);
+ guild.Serialize(writer);
+ var second = writer.Buffer.AsSpan(0, (int)writer.Position).ToArray();
- writer.Close();
- guild.Disband();
- leader.Delete();
+ Assert.Equal(first, second);
+
+ writer.Close();
+ }
+ finally
+ {
+ guild?.Disband();
+ leader.Delete();
+ }
}
[Fact]
public void RunMaintenanceRecalculatesStaleFealty()
{
var leader = CreatePlayer();
- var guild = new Guild(leader, "Maintenance Test Guild", "MTG");
- guild.LastFealty = Core.Now - TimeSpan.FromDays(3);
+ Guild guild = null;
- guild.RunMaintenance();
+ try
+ {
+ guild = new Guild(leader, "Maintenance Test Guild", "MTG");
+ guild.LastFealty = Core.Now - TimeSpan.FromDays(3);
- Assert.True(Core.Now - guild.LastFealty < TimeSpan.FromMinutes(1));
+ guild.RunMaintenance();
- guild.Disband();
- leader.Delete();
+ Assert.True(Core.Now - guild.LastFealty < TimeSpan.FromMinutes(1));
+ }
+ finally
+ {
+ guild?.Disband();
+ leader.Delete();
+ }
}
}
diff --git a/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs b/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
index 60b450b0d..233086f04 100644
--- a/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
+++ b/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
@@ -38,7 +38,7 @@ public class NameVerificationTests
[InlineData("Blacksmith 123")]
[InlineData("Baker-Smith")]
[InlineData("*Innkeeper*")]
- public void ValidateVendorName_ValidNames_ReturnsFalse(string name)
+ public void ValidateVendorName_DisallowedCharacters_ReturnsFalse(string name)
{
Assert.False(NameVerification.ValidateVendorName(name));
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/AcquisitionTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/AcquisitionTests.cs
index 3ee516dcf..e06571b63 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/AcquisitionTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/AcquisitionTests.cs
@@ -60,9 +60,14 @@ public class AcquisitionTests : IDisposable
bc.NextReacquireTime = Core.TickCount;
Assert.False(bc.AIObject.AcquireFocusMob(bc.RangePerception, FightMode.Closest, false, false, true));
- Assert.InRange(bc.NextReacquireTime - Core.TickCount, 5000, 10000);
+ Assert.Equal(ReacquireDelayMs(bc), bc.NextReacquireTime - Core.TickCount);
}
+ private static long ReacquireDelayMs(BaseCreature bc) => (long)bc.ReacquireDelay.TotalMilliseconds;
+
+ // A legal, fully armed deadline: as far out as a scan ever sets it. Movement can only pull it in.
+ private static void ArmFullDelay(BaseCreature bc) => bc.NextReacquireTime = Core.TickCount + ReacquireDelayMs(bc);
+
[Fact]
public void WedgedGate_SelfHeals()
{
@@ -78,7 +83,7 @@ public class AcquisitionTests : IDisposable
_created.Add(target);
// Illegal deadline (beyond ReacquireDelay): must read as open, not block forever.
- bc.NextReacquireTime = Core.TickCount + 60000;
+ bc.NextReacquireTime = Core.TickCount + ReacquireDelayMs(bc) * 6;
Assert.True(bc.AIObject.AcquireFocusMob(bc.RangePerception, FightMode.Closest, false, false, true));
Assert.Equal(target, bc.FocusMob);
@@ -96,7 +101,8 @@ public class AcquisitionTests : IDisposable
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var bc = Spawn(map, new Point3D(1500, 1600, (sbyte)z));
- bc.NextReacquireTime = Core.TickCount + 8000;
+ ArmFullDelay(bc);
+ var armed = bc.NextReacquireTime;
Mobile mover;
if (wildMover)
@@ -117,12 +123,12 @@ public class AcquisitionTests : IDisposable
if (notices)
{
- // Clamped to the approach delay (2s), never opened outright.
+ // Clamped to the approach delay, never opened outright.
Assert.InRange(remaining, 1, (long)bc.AcquireOnApproachDelay.TotalMilliseconds);
}
else
{
- Assert.True(remaining > 5000);
+ Assert.Equal(armed, bc.NextReacquireTime);
}
}
@@ -150,7 +156,7 @@ public class AcquisitionTests : IDisposable
bc.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map);
bc.AIObject.AITimer?.Stop();
_created.Add(bc);
- bc.NextReacquireTime = Core.TickCount + 8000;
+ ArmFullDelay(bc);
var mover = new TargetStub { Player = true };
mover.DefaultMobileInit();
@@ -173,7 +179,7 @@ public class AcquisitionTests : IDisposable
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var bc = Spawn(map, new Point3D(1500, 1600, (sbyte)z));
- bc.NextReacquireTime = Core.TickCount + 8000;
+ ArmFullDelay(bc);
var mover = new TargetStub { Player = true };
mover.DefaultMobileInit();
@@ -206,6 +212,6 @@ public class AcquisitionTests : IDisposable
Assert.True(bc.AIObject.AcquireFocusMob(bc.RangePerception, FightMode.Closest, false, false, true));
Assert.Equal(target, bc.FocusMob);
- Assert.True(bc.NextReacquireTime - Core.TickCount > 5000);
+ Assert.Equal(ReacquireDelayMs(bc), bc.NextReacquireTime - Core.TickCount);
}
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachOutcomeTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachOutcomeTests.cs
index fe8e397a1..55965375b 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachOutcomeTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachOutcomeTests.cs
@@ -1,43 +1,47 @@
+using System;
using System.Collections.Generic;
using Server;
using Server.Engines.Pathing.Cache;
using Server.Mobiles;
using Server.Tests;
-using Server.Tests.Mobiles.AI;
using Xunit;
namespace UOContent.Tests.Mobiles.AI;
// Driven manually against live map statics; the pathfinder's buffers are not reentrant.
[Collection("Sequential Pathfinding Tests")]
-public class ApproachOutcomeTests
+public class ApproachOutcomeTests : IDisposable
{
- private sealed class Stub : BaseCreature
+ private readonly List _created = [];
+
+ public void Dispose()
{
- public Stub(Serial serial) : base(serial) => Body = 0xC9;
+ for (var i = _created.Count - 1; i >= 0; i--)
+ {
+ _created[i].Delete();
+ }
+
+ _created.Clear();
}
- private sealed class Target : Mobile
- {
- public Target() => Body = 0xC9;
- }
-
- private static (Stub bc, BaseAI ai) NewFollower(Map map, int x, int y)
+ private (BareCreatureStub bc, BaseAI ai) NewFollower(Map map, int x, int y)
{
map.GetAverageZ(x, y, out _, out var z, out _);
- var bc = new Stub(World.NewMobile);
+ var bc = new BareCreatureStub(World.NewMobile);
bc.DefaultMobileInit();
bc.MoveToWorld(new Point3D(x, y, (sbyte)z), map);
+ _created.Add(bc);
BaseAI ai = new AnimalAI(bc);
ai.AITimer?.Stop();
return (bc, ai);
}
- private static Target NewTarget(Map map, int x, int y)
+ private WalkerStub NewTarget(Map map, int x, int y)
{
map.GetAverageZ(x, y, out _, out var z, out _);
- var t = new Target();
+ var t = new WalkerStub();
t.MoveToWorld(new Point3D(x, y, (sbyte)z), map);
+ _created.Add(t);
return t;
}
@@ -46,32 +50,24 @@ public class ApproachOutcomeTests
{
TileDataRequirement.SkipIfMissing();
var map = Map.Maps[1];
- var (bc, ai) = NewFollower(map, 1500, 1600);
+ var (_, ai) = NewFollower(map, 1500, 1600);
var target = NewTarget(map, 1497, 1600);
StepCache.Instance.Clear();
- try
- {
- ai.NextMove = 0;
- ai.MoveTo(target, 1);
- Assert.Equal(ApproachOutcome.DirectProgress, ai.LastApproach);
+ ai.NextMove = 0;
+ ai.MoveTo(target, 1);
+ Assert.Equal(ApproachOutcome.DirectProgress, ai.LastApproach);
- ai.MoveTo(target, 1); // budget consumed: no step
- Assert.Equal(ApproachOutcome.Waiting, ai.LastApproach);
+ ai.MoveTo(target, 1); // budget consumed: no step
+ Assert.Equal(ApproachOutcome.Waiting, ai.LastApproach);
- ai.NextMove = 0;
- ai.MoveTo(target, 1); // lands adjacent: still a progress step
- Assert.Equal(ApproachOutcome.DirectProgress, ai.LastApproach);
+ ai.NextMove = 0;
+ ai.MoveTo(target, 1); // lands adjacent: still a progress step
+ Assert.Equal(ApproachOutcome.DirectProgress, ai.LastApproach);
- ai.NextMove = 0;
- ai.MoveTo(target, 1);
- Assert.Equal(ApproachOutcome.Arrived, ai.LastApproach);
- }
- finally
- {
- bc.Delete();
- target.Delete();
- }
+ ai.NextMove = 0;
+ ai.MoveTo(target, 1);
+ Assert.Equal(ApproachOutcome.Arrived, ai.LastApproach);
}
[SkippableFact]
@@ -79,10 +75,9 @@ public class ApproachOutcomeTests
{
TileDataRequirement.SkipIfMissing();
var map = Map.Maps[1];
- var (bc, ai) = NewFollower(map, 1500, 1601);
+ var (_, ai) = NewFollower(map, 1500, 1601);
var target = NewTarget(map, 1500, 1596);
- var ring = new List
- ();
- var id = ApproachTargetTests.FirstImpassableItemId();
+ var id = TestTiles.FirstImpassableItemId();
Assert.NotEqual(0, id);
// Impassable ring around the target: unreachable.
@@ -96,35 +91,22 @@ public class ApproachOutcomeTests
}
map.GetAverageZ(x, y, out _, out var rz, out _);
- ring.Add(new Item(World.NewItem) { ItemID = id, Map = map, Location = new Point3D(x, y, (sbyte)rz) });
+ _created.Add(new Item(World.NewItem) { ItemID = id, Map = map, Location = new Point3D(x, y, (sbyte)rz) });
}
}
StepCache.Instance.Clear();
- try
+ var sawRouting = false;
+
+ for (var i = 0; i < 120; i++)
{
- var sawRouting = false;
-
- for (var i = 0; i < 120; i++)
- {
- ai.NextMove = 0;
- ai.MoveTo(target, 1);
- sawRouting |= ai.LastApproach is ApproachOutcome.Routing or ApproachOutcome.Blocked;
- }
-
- Assert.True(sawRouting, "a walled target must route or block before giving up");
- Assert.Equal(ApproachOutcome.GaveUp, ai.LastApproach);
+ ai.NextMove = 0;
+ ai.MoveTo(target, 1);
+ sawRouting |= ai.LastApproach is ApproachOutcome.Routing or ApproachOutcome.Blocked;
}
- finally
- {
- bc.Delete();
- target.Delete();
- for (var i = 0; i < ring.Count; i++)
- {
- ring[i].Delete();
- }
- }
+ Assert.True(sawRouting, "a walled target must route or block before giving up");
+ Assert.Equal(ApproachOutcome.GaveUp, ai.LastApproach);
}
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs
index f3869ffef..e330268f0 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using Server.Engines.Pathing.Cache;
using Server.Mobiles;
@@ -9,29 +10,45 @@ namespace Server.Tests.Mobiles.AI;
// pathfinder uses non-reentrant static buffers, so we share the pathfinding sequential
// collection to avoid parallel interference.
[Collection("Sequential Pathfinding Tests")]
-public class ApproachTargetTests
+public class ApproachTargetTests : IDisposable
{
- private sealed class FollowerStub : BaseCreature
+ private readonly List _created = [];
+
+ public void Dispose()
{
- // Serial ctor + DefaultMobileInit bypasses NPCSpeeds JSON (absent in tests).
- public FollowerStub(Serial serial) : base(serial) => Body = 0xC9;
+ for (var i = _created.Count - 1; i >= 0; i--)
+ {
+ _created[i].Delete();
+ }
+
+ _created.Clear();
}
- private sealed class TargetStub : Mobile
+ private (BareCreatureStub bc, BaseAI ai) NewFollower(Map map, Point3D loc)
{
- public TargetStub() => Body = 0xC9;
- }
-
- private static (FollowerStub bc, BaseAI ai) NewFollower(Map map, Point3D loc)
- {
- var bc = new FollowerStub(World.NewMobile);
+ var bc = new BareCreatureStub(World.NewMobile);
bc.DefaultMobileInit();
bc.MoveToWorld(loc, map);
+ _created.Add(bc);
BaseAI ai = new AnimalAI(bc);
ai.AITimer?.Stop(); // we drive movement manually; no background timer
return (bc, ai);
}
+ private WalkerStub NewTarget(Map map, Point3D loc)
+ {
+ var target = new WalkerStub();
+ target.MoveToWorld(loc, map);
+ _created.Add(target);
+ return target;
+ }
+
+ private void PlaceBlocker(Map map, ushort itemId, int x, int y)
+ {
+ map.GetAverageZ(x, y, out _, out var z, out _);
+ _created.Add(new Item(World.NewItem) { ItemID = itemId, Map = map, Location = new Point3D(x, y, (sbyte)z) });
+ }
+
// Drives the pet follow primitive once per tick; returns true once it reaches within
// arriveDist tiles. Uses InRange (Chebyshev) to match the game's own range semantics —
// a diagonal neighbor at Euclidean ~2.24 is "within 2" to the engine.
@@ -49,18 +66,6 @@ public class ApproachTargetTests
return false;
}
- internal static ushort FirstImpassableItemId()
- {
- for (ushort id = 1; id < TileData.MaxItemValue; id++)
- {
- if (TileData.ItemTable[id].ImpassableSurface)
- {
- return id;
- }
- }
- return 0;
- }
-
[Fact]
public void OpenTerrain_ReachesTarget_WithoutPathfinding()
{
@@ -69,8 +74,7 @@ public class ApproachTargetTests
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var (bc, ai) = NewFollower(map, new Point3D(1500, 1600, (sbyte)z));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1495, 1600, (sbyte)z), map); // 5 tiles west, open
+ var target = NewTarget(map, new Point3D(1495, 1600, (sbyte)z)); // 5 tiles west, open
StepCache.Instance.Clear();
var buildsBefore = StepCache.Instance.GetStats().BuildsTotal;
@@ -78,44 +82,40 @@ public class ApproachTargetTests
var arrived = DriveFollow(ai, bc, target, 2, 40);
var buildsAfter = StepCache.Instance.GetStats().BuildsTotal;
- bc.Delete();
- target.Delete();
Assert.True(arrived, "creature should reach an open-terrain target by stepping");
Assert.Equal(buildsBefore, buildsAfter); // greedy fast path: no chunk builds
}
- [Fact]
+ // The desk is a Britain inn static; on a blank map the detour is a straight walk.
+ [SkippableFact]
public void BritainInnDesk_PetReachesMaster()
{
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[1];
Assert.NotNull(map);
// Exact in-game repro: pet south of the L-desk, master north of it.
var (bc, ai) = NewFollower(map, new Point3D(1493, 1614, 20));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1494, 1605, 21), map);
+ var target = NewTarget(map, new Point3D(1494, 1605, 21));
StepCache.Instance.Clear();
// 200 ticks is generous for the ~17-step detour at one step per tick.
var arrived = DriveFollow(ai, bc, target, 2, 200);
- bc.Delete();
- target.Delete();
-
Assert.True(arrived, "pet must navigate around the L-desk to reach the master");
}
- [Fact]
+ [SkippableFact]
public void MoveTo_ReachesTargetAroundDesk()
{
+ TileDataRequirement.SkipIfMissing();
var map = Map.Maps[1];
Assert.NotNull(map);
var (bc, ai) = NewFollower(map, new Point3D(1493, 1614, 20));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1494, 1605, 21), map);
+ var target = NewTarget(map, new Point3D(1494, 1605, 21));
StepCache.Instance.Clear();
@@ -131,9 +131,6 @@ public class ApproachTargetTests
}
}
- bc.Delete();
- target.Delete();
-
Assert.True(arrived, "MoveTo chaser must reach the target around the desk");
}
@@ -145,8 +142,7 @@ public class ApproachTargetTests
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var (bc, ai) = NewFollower(map, new Point3D(1500, 1600, (sbyte)z));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1498, 1600, (sbyte)z), map);
+ var target = NewTarget(map, new Point3D(1498, 1600, (sbyte)z));
StepCache.Instance.Clear();
@@ -170,9 +166,6 @@ public class ApproachTargetTests
}
}
- bc.Delete();
- target.Delete();
-
Assert.True(caught, "chaser must catch a target that walks away then stops");
}
@@ -185,13 +178,11 @@ public class ApproachTargetTests
map.GetAverageZ(1500, 1601, out _, out var z, out _);
var (bc, ai) = NewFollower(map, new Point3D(1500, 1601, (sbyte)z));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1500, 1596, (sbyte)z), map);
+ var target = NewTarget(map, new Point3D(1500, 1596, (sbyte)z));
// Full impassable ring around the target cell -> no path to within range 1.
- var id = FirstImpassableItemId();
+ var id = TestTiles.FirstImpassableItemId();
Assert.NotEqual(0, id);
- var ring = new List
- ();
for (var x = 1499; x <= 1501; x++)
{
for (var y = 1595; y <= 1597; y++)
@@ -200,11 +191,8 @@ public class ApproachTargetTests
{
continue; // leave the target's own cell
}
- map.GetAverageZ(x, y, out _, out var rz, out _);
- ring.Add(new Item(World.NewItem)
- {
- ItemID = id, Map = map, Location = new Point3D(x, y, (sbyte)rz)
- });
+
+ PlaceBlocker(map, id, x, y);
}
}
@@ -231,16 +219,7 @@ public class ApproachTargetTests
}
}
- var reached = bc.InRange(target, 1);
-
- bc.Delete();
- target.Delete();
- foreach (var it in ring)
- {
- it.Delete();
- }
-
- Assert.False(reached, "walled-off target must not be reached");
+ Assert.False(bc.InRange(target, 1), "walled-off target must not be reached");
Assert.True(stayedIdle, "after giving up, the creature must idle, not shuffle");
}
@@ -253,33 +232,20 @@ public class ApproachTargetTests
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var (bc, ai) = NewFollower(map, new Point3D(1500, 1600, (sbyte)z));
- var target = new TargetStub();
- target.MoveToWorld(new Point3D(1500, 1595, (sbyte)z), map); // 5 tiles north
+ var target = NewTarget(map, new Point3D(1500, 1595, (sbyte)z)); // 5 tiles north
// Impassable wall on Y=1598 from X=1497..1503: blocks the straight line; ends open.
- var id = FirstImpassableItemId();
+ var id = TestTiles.FirstImpassableItemId();
Assert.NotEqual(0, id);
- var wall = new List
- ();
for (var x = 1497; x <= 1503; x++)
{
- map.GetAverageZ(x, 1598, out _, out var wz, out _);
- wall.Add(new Item(World.NewItem)
- {
- ItemID = id, Map = map, Location = new Point3D(x, 1598, (sbyte)wz)
- });
+ PlaceBlocker(map, id, x, 1598);
}
StepCache.Instance.Clear();
var arrived = DriveFollow(ai, bc, target, 2, 200);
- bc.Delete();
- target.Delete();
- foreach (var it in wall)
- {
- it.Delete();
- }
-
Assert.True(arrived, "creature must route around the wall to reach the target");
}
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/DetectHiddenTimerTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/DetectHiddenTimerTests.cs
index 383e2e103..84898b7f1 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/DetectHiddenTimerTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/DetectHiddenTimerTests.cs
@@ -1,6 +1,7 @@
using System;
using Server;
using Server.Mobiles;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Mobiles.AI;
@@ -37,9 +38,8 @@ public class DetectHiddenTimerTests
public void DetectHidden_SchedulesBoundedCooldown_AndWaitsBeforeRetry(
int intelligence, long minDelay, long maxDelay)
{
- Skip.If(!Server.Tests.TestServerInitializer.TileDataLoaded, "Requires UO client map data.");
- Core._tickCount = 0;
- Timer.Init(0);
+ Skip.If(!TestServerInitializer.TileDataLoaded, "Requires UO client map data.");
+ using var clock = new TimerWheelScope();
var creature = new DetectingCreature();
DetectingAI ai = null;
@@ -63,12 +63,8 @@ public class DetectHiddenTimerTests
Assert.InRange(ai._nextDetectHidden - Core.TickCount, minDelay, maxDelay);
Assert.True(ai.AITimer.Running);
- var deadline = ai._nextDetectHidden;
- while (Core.TickCount + 8 < deadline)
- {
- Core._tickCount += 8;
- Timer.Slice(Core.TickCount);
- }
+ // Stop one wheel step short of the cooldown: no retry may have fired yet.
+ TimerWheelScope.RunFor(ai._nextDetectHidden - TimerWheelScope.Step - Core.TickCount);
Assert.Equal(1, ai.Detections);
AdvanceUntil(() => ai.Detections > 1, 1024);
@@ -82,14 +78,9 @@ public class DetectHiddenTimerTests
}
}
- private static void AdvanceUntil(Func condition, int milliseconds)
- {
- for (var elapsed = 0; !condition() && elapsed < milliseconds; elapsed += 8)
- {
- Core._tickCount += 8;
- Timer.Slice(Core.TickCount);
- }
-
- Assert.True(condition(), "The AI timer did not reach the expected detection.");
- }
+ private static void AdvanceUntil(Func condition, int milliseconds) =>
+ Assert.True(
+ TimerWheelScope.RunUntil(condition, milliseconds),
+ "The AI timer did not reach the expected detection."
+ );
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/FamiliarAITests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/FamiliarAITests.cs
index 692ff8152..27b4ee0a0 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/FamiliarAITests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/FamiliarAITests.cs
@@ -4,8 +4,8 @@ using Server;
using Server.Items;
using Server.Mobiles;
using Server.Tests;
-using Server.Tests.Mobiles.AI;
using Xunit;
+using static Server.Tests.TimerWheelScope;
namespace UOContent.Tests.Mobiles.AI;
@@ -50,12 +50,12 @@ public class FamiliarAITests : IDisposable
private static readonly Map _map = Map.Maps[1];
private readonly List _created = new();
+ private readonly TimerWheelScope _clock;
public FamiliarAITests()
{
TileDataRequirement.SkipIfMissing();
- Core._tickCount = 0;
- Timer.Init(0);
+ _clock = new TimerWheelScope();
}
public void Dispose()
@@ -64,6 +64,8 @@ public class FamiliarAITests : IDisposable
{
_created[i].Delete();
}
+
+ _clock.Dispose();
}
private static Point3D At(int x, int y)
@@ -109,36 +111,6 @@ public class FamiliarAITests : IDisposable
return e;
}
- // 8ms lockstep keeps the wheel and Core.TickCount in sync.
- private static void RunFor(long ms)
- {
- var deadline = Core._tickCount + ms;
-
- while (Core._tickCount - deadline < 0)
- {
- Core._tickCount += 8;
- Timer.Slice(Core._tickCount);
- }
- }
-
- private static bool RunUntil(Func condition, long maxMs)
- {
- var deadline = Core._tickCount + maxMs;
-
- while (Core._tickCount - deadline < 0)
- {
- if (condition())
- {
- return true;
- }
-
- Core._tickCount += 8;
- Timer.Slice(Core._tickCount);
- }
-
- return condition();
- }
-
[SkippableFact]
public void Follows_WithoutBacktracking()
{
@@ -323,7 +295,7 @@ public class FamiliarAITests : IDisposable
var f = Familiar(0, p, 1500, 1602);
// 5x5 ring, open 3x3 interior: a landing tile exists, no route reaches it.
- var id = ApproachTargetTests.FirstImpassableItemId();
+ var id = TestTiles.FirstImpassableItemId();
Assert.NotEqual(0, id);
for (var x = 1498; x <= 1502; x++)
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs
index 785278507..f76a0e4db 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using Server;
using Server.Mobiles;
@@ -7,8 +8,20 @@ namespace UOContent.Tests.Mobiles.AI;
// Guard-following may pathfind, so this shares the pathfinding collection.
[Collection("Sequential Pathfinding Tests")]
-public class GuardFollowTests
+public class GuardFollowTests : IDisposable
{
+ private readonly List _created = [];
+
+ public void Dispose()
+ {
+ for (var i = 0; i < _created.Count; i++)
+ {
+ _created[i].Delete();
+ }
+
+ _created.Clear();
+ }
+
[Fact]
public void GuardFollow_StepsTowardMaster_AndRegistersMoveIntent()
{
@@ -16,13 +29,14 @@ public class GuardFollowTests
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); // 6 tiles east, open terrain
- pet.SetControlMaster(master);
+ // Pet 6 tiles east of the master, open terrain.
+ var (master, pet) = PetTestSetup.SpawnControlledPet(
+ new Point3D(1494, 1600, (sbyte)z),
+ new Point3D(1500, 1600, (sbyte)z),
+ map
+ );
+ _created.Add(master);
+ _created.Add(pet);
var ai = pet.AIObject;
ai.AITimer?.Stop(); // drive manually
@@ -33,21 +47,13 @@ public class GuardFollowTests
ai.NextMove = 0;
ai.Obey();
- var moved = pet.Location != start;
- var hasIntent = ai.TryGetMoveWake(out _);
- var currentSpeed = pet.CurrentSpeed;
- var currentMoveSpeed = pet.CurrentMoveSpeed;
-
- pet.Delete();
- master.Delete();
-
- Assert.True(moved, "a guarding pet beyond guard range must step toward its master");
+ Assert.True(pet.Location != start, "a guarding pet beyond guard range must step toward its master");
// Without a move intent, guard-following only steps on the think grid.
- Assert.True(hasIntent, "guard-following must register a move intent");
+ Assert.True(ai.TryGetMoveWake(out _), "guard-following must register a move intent");
// The return is paced by FollowMoveSpeed; the think clock stays on the active value
// and the per-step speed flip must not undo either.
- Assert.Equal(0.2, currentSpeed);
- Assert.Equal(0.1, currentMoveSpeed);
+ Assert.Equal(0.2, pet.CurrentSpeed);
+ Assert.Equal(pet.FollowMoveSpeed, pet.CurrentMoveSpeed);
}
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs
index ec11a3ba5..ee3bfe7cd 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs
@@ -11,10 +11,24 @@ public class GuardFriendlyFireTests : IDisposable
{
private readonly List _created = [];
+ // AI_Mage stands in for a shipped caster: MageAI's combat loop has its own reacquire path.
+ private sealed class MagePet : BaseCreature
+ {
+ public MagePet() : base(AIType.AI_Mage, FightMode.Closest, 10, 1) => Body = 0xC8;
+
+ public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
+ {
+ activeSpeed = 0.2;
+ passiveSpeed = 0.4;
+ }
+ }
+
private (PlayerMobile owner, PetTestStub pet) Spawn()
{
Skip.If(!Server.Tests.TestServerInitializer.TileDataLoaded, "Requires UO client map data.");
var z = Map.Felucca.GetAverageZ(1500, 1600);
+ // The constructed PlayerMobile, not PetTestSetup's deserialization-shaped one: the harmful
+ // checks read PermaFlags, which only the full constructor allocates.
var owner = new PlayerMobile();
owner.MoveToWorld(new Point3D(1500, 1600, z), Map.Felucca);
_created.Add(owner);
@@ -192,28 +206,28 @@ public class GuardFriendlyFireTests : IDisposable
}
[SkippableFact]
- public void ActualWhiteWyrmDoesNotTurnOnOwnerWhenEnemyDisappears()
+ public void GuardingMageDoesNotTurnOnOwnerWhenEnemyDisappears()
{
var (owner, _) = Spawn();
- var wyrm = new WhiteWyrm();
- _created.Add(wyrm);
- wyrm.MoveToWorld(new Point3D(1498, 1600, owner.Z), owner.Map);
- wyrm.SetControlMaster(owner);
- wyrm.IssueOrder(OrderType.Guard, owner);
- wyrm.AIObject.AITimer.Stop();
+ var mage = new MagePet();
+ _created.Add(mage);
+ mage.MoveToWorld(new Point3D(1498, 1600, owner.Z), owner.Map);
+ mage.SetControlMaster(owner);
+ mage.IssueOrder(OrderType.Guard, owner);
+ mage.AIObject.AITimer.Stop();
var enemy = new PetTestStub();
_created.Add(enemy);
enemy.MoveToWorld(new Point3D(1497, 1600, owner.Z), owner.Map);
enemy.AIObject.AITimer.Stop();
- wyrm.Combatant = enemy;
+ mage.Combatant = enemy;
enemy.Hidden = true;
- wyrm.NextReacquireTime = Core.TickCount - 1;
- Assert.IsType(wyrm.AIObject);
- wyrm.AIObject.DoActionCombat();
- Assert.NotSame(owner, wyrm.Combatant);
- Assert.Null(wyrm.FocusMob);
- wyrm.AIObject.DoOrderGuard();
- Assert.Null(wyrm.Combatant);
+ mage.NextReacquireTime = Core.TickCount - 1;
+ Assert.IsType(mage.AIObject);
+ mage.AIObject.DoActionCombat();
+ Assert.NotSame(owner, mage.Combatant);
+ Assert.Null(mage.FocusMob);
+ mage.AIObject.DoOrderGuard();
+ Assert.Null(mage.Combatant);
}
public void Dispose()
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs
index 7d572786a..356994020 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Server;
using Server.Mobiles;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Mobiles.AI;
@@ -14,11 +15,6 @@ public class GuardOrderTests : IDisposable
{
private readonly List _created = new();
- private sealed class AggressorStub : Mobile
- {
- public AggressorStub() => Body = 0xC9;
- }
-
public void Dispose()
{
foreach (var m in _created)
@@ -35,14 +31,12 @@ public class GuardOrderTests : IDisposable
Assert.NotNull(map);
map.GetAverageZ(1500, 1600, out _, out z, out _);
- var master = new PlayerMobile(World.NewMobile);
- master.DefaultMobileInit();
- master.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map);
+ var (master, pet) = PetTestSetup.SpawnControlledPet(
+ new Point3D(1500, 1600, (sbyte)z),
+ new Point3D(1499, 1600, (sbyte)z),
+ map
+ );
_created.Add(master);
-
- var pet = new PetTestStub();
- pet.MoveToWorld(new Point3D(1499, 1600, (sbyte)z), map);
- pet.SetControlMaster(master);
_created.Add(pet);
pet.AIObject.AITimer?.Stop(); // drive manually
@@ -52,9 +46,9 @@ public class GuardOrderTests : IDisposable
return (master, pet);
}
- private AggressorStub SpawnAggressor(PetTestStub pet, Point3D loc, Mobile attacking)
+ private WalkerStub SpawnAggressor(PetTestStub pet, Point3D loc, Mobile attacking)
{
- var aggr = new AggressorStub();
+ var aggr = new WalkerStub();
aggr.MoveToWorld(loc, pet.Map);
_created.Add(aggr);
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/HerdingTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/HerdingTests.cs
index 357a39979..6c88649fa 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/HerdingTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/HerdingTests.cs
@@ -10,11 +10,6 @@ namespace UOContent.Tests.Mobiles.AI;
[Collection("Sequential Pathfinding Tests")]
public class HerdingTests
{
- private sealed class Stub : BaseCreature
- {
- public Stub(Serial serial) : base(serial) => Body = 0xC9;
- }
-
[SkippableFact]
public void HerdedCreature_ReachesTheTile_AndClearsTargetLocation()
{
@@ -22,7 +17,7 @@ public class HerdingTests
var map = Map.Maps[1];
map.GetAverageZ(1500, 1600, out _, out var z, out _);
- var bc = new Stub(World.NewMobile);
+ var bc = new BareCreatureStub(World.NewMobile);
bc.DefaultMobileInit();
bc.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map);
BaseAI ai = new AnimalAI(bc);
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs
index 2789cd735..288572eb7 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs
@@ -210,10 +210,10 @@ public class PetOrderTests : IDisposable
Assert.Equal(post, pet.Home); // anchor still the original post, not the corpse
}
- // NOTE: the test fixture does not load tile data, so Mobile.Move is blocked and Location
- // never changes here. DoMoveImpl still sets Mobile.Direction before the (blocked) move,
- // so an *attempted* wander is observable via Direction. The subjective wander cadence is
- // covered by manual QA; these tests verify the gate/frozen wiring deterministically.
+ // A step may be blocked by whatever terrain (1000, 1000) holds, but DoMoveImpl sets
+ // Mobile.Direction before attempting the move, so an *attempted* wander is observable via
+ // Direction either way. The subjective wander cadence is covered by manual QA; these tests
+ // verify the gate/frozen wiring deterministically.
[Fact]
public void IdlePet_DoesNotAttemptToMove_WhileResting()
{
@@ -224,12 +224,18 @@ public class PetOrderTests : IDisposable
pet.ForceIdle = true; // CheckIdle() reports resting -> idle wander must be skipped
pet.Direction = Direction.North;
+ var start = pet.Location;
+ var ai = pet.AIObject;
for (var i = 0; i < 40; i++)
{
- pet.AIObject.DoOrderNone();
+ // Reopen the move clock every pass, so CheckIdle is the only gate left standing.
+ ai.NextMove = Core.TickCount;
+ Assert.True(ai.CanMoveNow(out _));
+ ai.DoOrderNone();
}
Assert.Equal(Direction.North, pet.Direction); // gated by CheckIdle -> never attempts a step
+ Assert.Equal(start, pet.Location);
}
[Fact]
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.cs
index 1064fb317..793fd3d6f 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.cs
@@ -2,7 +2,9 @@ using System;
using System.Collections.Generic;
using Server;
using Server.Mobiles;
+using Server.Tests;
using Xunit;
+using static Server.Tests.TimerWheelScope;
namespace UOContent.Tests.Mobiles.AI;
@@ -11,6 +13,7 @@ namespace UOContent.Tests.Mobiles.AI;
public class PetPacingTests : IDisposable
{
private readonly List _created = new();
+ private TimerWheelScope _clock;
private (PlayerMobile master, PetTestStub pet) Spawn(Point3D masterLoc, Point3D petLoc)
{
@@ -28,6 +31,7 @@ public class PetPacingTests : IDisposable
}
_created.Clear();
+ _clock?.Dispose();
}
// Movement orders run active, resting orders run passive; the move clock follows.
@@ -64,8 +68,8 @@ public class PetPacingTests : IDisposable
pet.ControlTarget = master;
pet.ControlOrder = OrderType.Follow; // fixture era is EJ
- Assert.Equal(0.2, pet.CurrentSpeed); // active think, not the follow pace
- Assert.Equal(0.1, pet.CurrentMoveSpeed); // capped at the follow pace
+ Assert.Equal(0.2, pet.CurrentSpeed); // active think, not the follow pace
+ Assert.Equal(pet.FollowMoveSpeed, pet.CurrentMoveSpeed); // capped at the follow pace
}
// A creature configured faster than the follow pace keeps its own.
@@ -73,12 +77,13 @@ public class PetPacingTests : IDisposable
public void FollowMaster_KeepsAFasterConfiguredPace()
{
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
- pet.SetMoveSpeed(0.05, 0.9);
+ var faster = pet.FollowMoveSpeed / 2;
+ pet.SetMoveSpeed(faster, 0.9);
pet.ControlTarget = master;
pet.ControlOrder = OrderType.Follow;
- Assert.Equal(0.05, pet.CurrentMoveSpeed);
+ Assert.Equal(faster, pet.CurrentMoveSpeed);
}
// The move-clock override survives the order: it is capped while following, not overwritten.
@@ -145,7 +150,7 @@ public class PetPacingTests : IDisposable
pet.ControlOrder = OrderType.Guard;
Assert.Equal(0.2, pet.CurrentSpeed);
- Assert.Equal(0.1, pet.CurrentMoveSpeed);
+ Assert.Equal(pet.FollowMoveSpeed, pet.CurrentMoveSpeed);
}
// Obeying the follow order must not write the pace into either clock.
@@ -161,7 +166,7 @@ public class PetPacingTests : IDisposable
pet.AIObject.Obey();
Assert.Equal(0.2, pet.CurrentSpeed);
- Assert.Equal(0.1, pet.CurrentMoveSpeed);
+ Assert.Equal(pet.FollowMoveSpeed, pet.CurrentMoveSpeed);
}
// At the master's side a guarding pet stays active: no stale-warmode passive, no sprint.
@@ -208,41 +213,10 @@ public class PetPacingTests : IDisposable
return (master, pet);
}
- // Advances time in 8ms lockstep so the wheel and Core.TickCount stay in sync.
- private static void RunFor(long ms)
- {
- var deadline = Core._tickCount + ms;
-
- while (Core._tickCount < deadline)
- {
- Core._tickCount += 8;
- Timer.Slice(Core._tickCount);
- }
- }
-
- private static bool RunUntil(Func condition, long maxMs)
- {
- var deadline = Core._tickCount + maxMs;
-
- while (Core._tickCount < deadline)
- {
- if (condition())
- {
- return true;
- }
-
- Core._tickCount += 8;
- Timer.Slice(Core._tickCount);
- }
-
- return condition();
- }
-
// Runs past the spawn stagger; returns right after a think with the next 0.4s away.
private ThinkProbe SettledProbe(out PlayerMobile master)
{
- Core._tickCount = 0;
- Timer.Init(0);
+ _clock = new TimerWheelScope();
var (m, pet) = SpawnProbe();
master = m;
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs
index 41b52a189..dec588d49 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs
@@ -35,16 +35,10 @@ public class PetRetaliationTests : IDisposable
private (PlayerMobile master, T pet) Spawn() where T : BaseCreature, new()
{
- var master = new PlayerMobile(World.NewMobile);
- master.DefaultMobileInit();
- master.MoveToWorld(new Point3D(1000, 1000, 0), Map.Felucca);
+ var (master, pet) = PetTestSetup.SpawnControlledPet(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
_created.Add(master);
-
- var pet = new T();
- pet.MoveToWorld(new Point3D(1001, 1000, 0), Map.Felucca);
- pet.SetControlMaster(master);
- pet.AIObject.AITimer?.Stop();
_created.Add(pet);
+ pet.AIObject.AITimer?.Stop();
return (master, pet);
}
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetTestStub.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetTestStub.cs
index d15228590..7779384b6 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetTestStub.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetTestStub.cs
@@ -49,17 +49,21 @@ public class PetTestStub : BaseCreature
public static class PetTestSetup
{
- // Places a player master and a controlled pet on Felucca and returns both.
+ // Places a player master and a controlled pet (on Felucca unless a map is given) and returns both.
public static (PlayerMobile master, PetTestStub pet) SpawnControlledPet(
- Point3D masterLoc, Point3D petLoc)
+ Point3D masterLoc, Point3D petLoc, Map map = null) =>
+ SpawnControlledPet(masterLoc, petLoc, map);
+
+ public static (PlayerMobile master, T pet) SpawnControlledPet(
+ Point3D masterLoc, Point3D petLoc, Map map = null) where T : BaseCreature, new()
{
- var map = Map.Felucca;
+ map ??= Map.Felucca;
var master = new PlayerMobile(World.NewMobile);
master.DefaultMobileInit();
master.MoveToWorld(masterLoc, map);
- var pet = new PetTestStub();
+ var pet = new T();
pet.MoveToWorld(petLoc, map);
pet.SetControlMaster(master); // sets Controlled, Home=Zero, ControlOrder=Come
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs
index ba106218c..5d92fca85 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Server;
using Server.Items;
using Server.Mobiles;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Mobiles;
@@ -15,60 +16,14 @@ namespace UOContent.Tests.Mobiles;
[Collection("Sequential UOContent Tests")]
public class BaseCreatureSerializationTests : IDisposable
{
- private readonly List _created = new();
- private readonly List
- _createdItems = new();
+ private readonly MobileTestHarness _harness = new();
- public void Dispose()
- {
- for (var i = 0; i < _created.Count; i++)
- {
- _created[i].Delete();
- }
+ public void Dispose() => _harness.Dispose();
- for (var i = 0; i < _createdItems.Count; i++)
- {
- _createdItems[i].Delete();
- }
- }
-
- private class CreatureStub : BaseCreature
- {
- public CreatureStub() : base(AIType.AI_Melee) => Body = 0xC9;
-
- public CreatureStub(Serial serial) : base(serial) => Body = 0xC9;
-
- public DateTime SummonEndValue => SummonEnd;
-
- // Stands in for the npc-speeds table (unconfigured in the test fixture).
- public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
- {
- activeSpeed = 0.3;
- passiveSpeed = 0.6;
- }
-
- public override void GetMoveSpeeds(out double activeMoveSpeed, out double passiveMoveSpeed)
- {
- activeMoveSpeed = 0.6;
- passiveMoveSpeed = 1.2;
- }
- }
-
- private CreatureStub NewCreature()
- {
- var bc = new CreatureStub();
- _created.Add(bc);
- return bc;
- }
+ private TestCreature NewCreature() => _harness.NewCreature();
// ReadEntity resolves references through the world table, so the master must be registered.
- private PlayerMobile NewMaster()
- {
- var master = new PlayerMobile(World.NewMobile);
- master.DefaultMobileInit();
- World.AddEntity(master);
- _created.Add(master);
- return master;
- }
+ private PlayerMobile NewMaster() => _harness.NewPlayer();
private static byte[] Snapshot(Mobile m)
{
@@ -80,10 +35,10 @@ public class BaseCreatureSerializationTests : IDisposable
return buffer;
}
- private CreatureStub Load(byte[] buffer)
+ private TestCreature Load(byte[] buffer)
{
- var copy = new CreatureStub(World.NewMobile);
- _created.Add(copy);
+ var copy = new TestCreature(World.NewMobile);
+ _harness.Track(copy);
var reader = new BufferReader(buffer);
copy.Deserialize(reader);
@@ -219,7 +174,7 @@ public class BaseCreatureSerializationTests : IDisposable
var bc = NewCreature();
var friend = NewMaster();
var wayPoint = new WayPoint();
- _createdItems.Add(wayPoint);
+ _harness.TrackItem(wayPoint);
bc.AddPetFriend(friend);
bc.CurrentWayPoint = wayPoint;
@@ -245,49 +200,20 @@ public class BaseCreatureSerializationTests : IDisposable
Assert.InRange(copy.DeleteTimeLeft, TimeSpan.FromDays(3.0) - TimeSpan.FromSeconds(5), TimeSpan.FromDays(3.0));
}
- private sealed class VendorStub : BaseVendor
- {
- private static readonly List _sbInfos = [];
-
- public VendorStub() : base("the stub")
- {
- }
-
- public VendorStub(Serial serial) : base(serial)
- {
- }
-
- protected override List SBInfos => _sbInfos;
-
- public override void InitSBInfo()
- {
- }
-
- public override void InitOutfit()
- {
- }
-
- public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
- {
- activeSpeed = 0.3;
- passiveSpeed = 0.6;
- }
- }
-
// BaseVendor is generated on top of the generated BaseCreature; the chain must write
// and read both sections in order with exact consumption.
[Fact]
public void GeneratedVendorChain_RoundTrips()
{
- var vendor = new VendorStub();
- _created.Add(vendor);
+ var vendor = new TestVendor();
+ _harness.Track(vendor);
vendor.Home = new Point3D(1500, 1600, 0);
vendor.RangeHome = 2;
var buffer = Snapshot(vendor);
- var copy = new VendorStub(World.NewMobile);
- _created.Add(copy);
+ var copy = new TestVendor(World.NewMobile);
+ _harness.Track(copy);
var reader = new BufferReader(buffer);
copy.Deserialize(reader);
@@ -378,7 +304,7 @@ public class BaseCreatureSerializationTests : IDisposable
writer.Write(0.0); // passive move speed (v22)
}
- private CreatureStub LoadLegacyV22(
+ private TestCreature LoadLegacyV22(
bool controlled = false,
Mobile controlMaster = null,
bool summoned = false,
@@ -390,7 +316,7 @@ public class BaseCreatureSerializationTests : IDisposable
// Mobile donor produces a byte-authentic one.
var donor = new MobileStub();
donor.DefaultMobileInit();
- _created.Add(donor);
+ _harness.Track(donor);
var writer = new BufferWriter(true);
donor.Serialize(writer);
@@ -399,8 +325,8 @@ public class BaseCreatureSerializationTests : IDisposable
var buffer = new byte[writer.Position];
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
- var copy = new CreatureStub(World.NewMobile);
- _created.Add(copy);
+ var copy = new TestCreature(World.NewMobile);
+ _harness.Track(copy);
var reader = new BufferReader(buffer);
copy.Deserialize(reader);
@@ -499,7 +425,7 @@ public class BaseCreatureSerializationTests : IDisposable
var donor = new MobileStub();
donor.DefaultMobileInit();
- _created.Add(donor);
+ _harness.Track(donor);
var writer = new BufferWriter(true);
donor.Serialize(writer);
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/DeletedMasterTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/DeletedMasterTests.cs
index f01e5c7bd..be314b575 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/DeletedMasterTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/DeletedMasterTests.cs
@@ -1,8 +1,8 @@
using System;
-using System.Collections.Generic;
using Server;
using Server.Mobiles;
using Server.Spells.Necromancy;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Mobiles;
@@ -11,40 +11,13 @@ namespace UOContent.Tests.Mobiles;
[Collection("Sequential UOContent Tests")]
public class DeletedMasterTests : IDisposable
{
- private readonly List _created = [];
+ private readonly MobileTestHarness _harness = new();
- public void Dispose()
- {
- for (var i = 0; i < _created.Count; i++)
- {
- _created[i].Delete();
- }
- }
+ public void Dispose() => _harness.Dispose();
- private sealed class Stub : BaseCreature
- {
- public Stub() : base(AIType.AI_Melee) => Body = 0xC9;
+ private TestCreature NewCreature() => _harness.NewCreature();
- public Stub(Serial serial) : base(serial)
- {
- }
- }
-
- private Stub NewCreature()
- {
- var bc = new Stub();
- _created.Add(bc);
- return bc;
- }
-
- private PlayerMobile NewPlayer()
- {
- var pm = new PlayerMobile(World.NewMobile);
- pm.DefaultMobileInit();
- World.AddEntity(pm);
- _created.Add(pm);
- return pm;
- }
+ private PlayerMobile NewPlayer() => _harness.NewPlayer();
[Fact]
public void DeletingAPlayer_ReleasesAnUnbondedPet()
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs
index 20578ecfc..bb027a951 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs
@@ -1,7 +1,7 @@
using System;
-using System.Collections.Generic;
using Server;
using Server.Mobiles;
+using Server.Tests;
using Xunit;
namespace UOContent.Tests.Mobiles;
@@ -10,40 +10,13 @@ namespace UOContent.Tests.Mobiles;
[Collection("Sequential UOContent Tests")]
public class MasterViewsTests : IDisposable
{
- private readonly List _created = [];
+ private readonly MobileTestHarness _harness = new();
- public void Dispose()
- {
- for (var i = 0; i < _created.Count; i++)
- {
- _created[i].Delete();
- }
- }
+ public void Dispose() => _harness.Dispose();
- private sealed class Stub : BaseCreature
- {
- public Stub() : base(AIType.AI_Melee) => Body = 0xC9;
+ private TestCreature NewCreature() => _harness.NewCreature();
- public Stub(Serial serial) : base(serial)
- {
- }
- }
-
- private Stub NewCreature()
- {
- var bc = new Stub();
- _created.Add(bc);
- return bc;
- }
-
- private PlayerMobile NewPlayer()
- {
- var pm = new PlayerMobile(World.NewMobile);
- pm.DefaultMobileInit();
- World.AddEntity(pm);
- _created.Add(pm);
- return pm;
- }
+ private PlayerMobile NewPlayer() => _harness.NewPlayer();
public enum Shape
{
diff --git a/Projects/UOContent.Tests/Tests/Mobiles/VendorBuybackTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/VendorBuybackTests.cs
index f61ca6229..fac93cb54 100644
--- a/Projects/UOContent.Tests/Tests/Mobiles/VendorBuybackTests.cs
+++ b/Projects/UOContent.Tests/Tests/Mobiles/VendorBuybackTests.cs
@@ -9,15 +9,9 @@ namespace Server.Tests.Mobiles;
[Collection("Sequential UOContent Tests")]
public class VendorBuybackTests : IDisposable
{
- private readonly List _created = [];
+ private readonly MobileTestHarness _harness = new();
- public void Dispose()
- {
- for (var i = 0; i < _created.Count; i++)
- {
- _created[i].Delete();
- }
- }
+ public void Dispose() => _harness.Dispose();
private sealed class BandageSB : SBInfo
{
@@ -30,34 +24,9 @@ public class VendorBuybackTests : IDisposable
public override List BuyInfo { get; } = [];
}
- private sealed class VendorStub : BaseVendor
+ private sealed class BandageVendor : TestVendor
{
- private readonly List _sbInfos = [];
-
- public VendorStub() : base("the stub")
- {
- }
-
- public VendorStub(Serial serial) : base(serial)
- {
- }
-
- protected override List SBInfos => _sbInfos;
-
- public override void InitSBInfo()
- {
- _sbInfos.Add(new BandageSB());
- }
-
- public override void InitOutfit()
- {
- }
-
- public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
- {
- activeSpeed = 0.3;
- passiveSpeed = 0.6;
- }
+ protected override void AddSBInfos(List sbInfos) => sbInfos.Add(new BandageSB());
}
// Same sell price as BandageSB, but a buy list at the 250-entry limit so buyback capacity is 0.
@@ -81,55 +50,36 @@ public class VendorBuybackTests : IDisposable
public override List BuyInfo => _buyInfo;
}
- private sealed class FullStockVendorStub : BaseVendor
+ private sealed class FullStockVendor : TestVendor
{
- private readonly List _sbInfos = [];
-
- public FullStockVendorStub() : base("the stub")
- {
- }
-
- protected override List SBInfos => _sbInfos;
-
- public override void InitSBInfo()
- {
- _sbInfos.Add(new FullStockSB());
- }
-
- public override void InitOutfit()
- {
- }
-
- public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
- {
- activeSpeed = 0.3;
- passiveSpeed = 0.6;
- }
+ protected override void AddSBInfos(List sbInfos) => sbInfos.Add(new FullStockSB());
}
- private VendorStub NewVendor()
- {
- var vendor = new VendorStub();
- _created.Add(vendor);
- return vendor;
- }
+ private BandageVendor NewVendor() => _harness.Track(new BandageVendor());
- private FullStockVendorStub NewFullStockVendor()
- {
- var vendor = new FullStockVendorStub();
- _created.Add(vendor);
- return vendor;
- }
+ private FullStockVendor NewFullStockVendor() => _harness.Track(new FullStockVendor());
private Mobile NewSeller()
{
- var seller = new Mobile(World.NewMobile);
+ var seller = _harness.Track(new Mobile(World.NewMobile));
seller.DefaultMobileInit();
seller.AddItem(new Backpack());
- _created.Add(seller);
return seller;
}
+ // A fresh seller offers a stack of 10 bandages and sells of them.
+ private Bandage SellBandages(BaseVendor vendor, int amount, out Mobile seller)
+ {
+ seller = NewSeller();
+ var bandages = new Bandage(10);
+ seller.Backpack.DropItem(bandages);
+
+ Assert.True(vendor.OnSellItems(seller, [new SellItemResponse(bandages, amount)]));
+ return bandages;
+ }
+
+ private Bandage SellBandages(BaseVendor vendor, int amount) => SellBandages(vendor, amount, out _);
+
[Theory]
[InlineData(0.25, 1.0)] // restocked 15m ago: next boundary is the regular one
[InlineData(5.0, 6.0)] // exactly on a boundary: strictly after now
@@ -160,11 +110,7 @@ public class VendorBuybackTests : IDisposable
public void PartialStackSale_SplitGoesToBuyback_SellerKeepsRemainder()
{
var vendor = NewVendor();
- var seller = NewSeller();
- var bandages = new Bandage(10);
- seller.Backpack.DropItem(bandages);
-
- Assert.True(vendor.OnSellItems(seller, [new SellItemResponse(bandages, 4)]));
+ var bandages = SellBandages(vendor, 4, out var seller);
Assert.Equal(6, bandages.Amount);
Assert.Same(seller.Backpack, bandages.Parent);
@@ -181,10 +127,7 @@ public class VendorBuybackTests : IDisposable
public void Restock_PurgesBuyback()
{
var vendor = NewVendor();
- var seller = NewSeller();
- var bandages = new Bandage(10);
- seller.Backpack.DropItem(bandages);
- vendor.OnSellItems(seller, [new SellItemResponse(bandages, 10)]);
+ var bandages = SellBandages(vendor, 10);
Assert.Single(vendor.BuyPack.Items);
vendor.Restock();
@@ -197,20 +140,14 @@ public class VendorBuybackTests : IDisposable
public void Restock_ClearsPurgeSchedule_AndASubsequentSaleRearmsIt()
{
var vendor = NewVendor();
- var seller = NewSeller();
- var bandages = new Bandage(10);
- seller.Backpack.DropItem(bandages);
- vendor.OnSellItems(seller, [new SellItemResponse(bandages, 10)]);
+ SellBandages(vendor, 10);
Assert.True(vendor.BuybackPurgeScheduled);
vendor.Restock();
Assert.False(vendor.BuybackPurgeScheduled);
- var seller2 = NewSeller();
- var moreBandages = new Bandage(10);
- seller2.Backpack.DropItem(moreBandages);
- vendor.OnSellItems(seller2, [new SellItemResponse(moreBandages, 10)]);
+ SellBandages(vendor, 10);
Assert.True(vendor.BuybackPurgeScheduled);
}
@@ -219,10 +156,7 @@ public class VendorBuybackTests : IDisposable
public void Delete_CancelsBuybackPurgeTimer()
{
var vendor = NewVendor();
- var seller = NewSeller();
- var bandages = new Bandage(10);
- seller.Backpack.DropItem(bandages);
- vendor.OnSellItems(seller, [new SellItemResponse(bandages, 10)]);
+ SellBandages(vendor, 10);
Assert.True(vendor.BuybackPurgeScheduled);
vendor.Delete();
@@ -239,7 +173,8 @@ public class VendorBuybackTests : IDisposable
var legacy = new Backpack { Layer = Layer.ShopBuy, Movable = false, Visible = false };
vendor.AddItem(legacy);
- var children = new Item[20_000];
+ // Several children, so a delete loop that skips entries as the list shrinks would show.
+ var children = new Item[3];
for (var i = 0; i < children.Length; i++)
{
children[i] = new Item(0x1234);
@@ -261,11 +196,7 @@ public class VendorBuybackTests : IDisposable
public void SaleWithNoBuybackCapacity_ConsumesItem_AndStillPays()
{
var vendor = NewFullStockVendor();
- var seller = NewSeller();
- var bandages = new Bandage(10);
- seller.Backpack.DropItem(bandages);
-
- Assert.True(vendor.OnSellItems(seller, [new SellItemResponse(bandages, 10)]));
+ var bandages = SellBandages(vendor, 10, out var seller);
Assert.True(bandages.Deleted);
Assert.Empty(vendor.BuyPack.Items);
diff --git a/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs b/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs
index b981d9f86..d603a0c12 100644
--- a/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs
+++ b/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs
@@ -216,7 +216,8 @@ public sealed class DesignStateDetailed : Packet
if (deflatedLength == 0)
{
- Console.WriteLine("Compression error");
+ // An oracle that silently emits a truncated plane would make the comparison meaningless.
+ throw new InvalidOperationException("Compression error");
}
Write((byte)(0x20 | i));
@@ -248,7 +249,8 @@ public sealed class DesignStateDetailed : Packet
if (deflatedLength == 0)
{
- Console.WriteLine("Compression error");
+ // An oracle that silently emits a truncated plane would make the comparison meaningless.
+ throw new InvalidOperationException("Compression error");
}
Write((byte)(9 + i));
diff --git a/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs b/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs
index b3a6d291f..2faa8eba5 100644
--- a/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs
@@ -13,7 +13,9 @@
* along with this program. If not, see . *
*************************************************************************/
+using System;
using System.Net;
+using System.Reflection;
using System.Text;
using Server.Network;
using Server.Network.Bans;
@@ -21,10 +23,18 @@ using Xunit;
namespace Server.Tests.Network.Exemptions;
-// Addresses come from TEST-NET-1 (192.0.2.0/24) to stay clear of the other ban test classes if xUnit runs
-// them concurrently.
-public class BanExemptionsTests
+// ManualAllowlist is process-wide and BlocklistFilter reads it, so this runs in the sequential collection
+// and puts the original snapshot back. Addresses come from TEST-NET-1 (192.0.2.0/24).
+[Collection("Sequential UOContent Tests")]
+public class BanExemptionsTests : IDisposable
{
+ private static readonly FieldInfo _snapshotField =
+ typeof(ManualAllowlist).GetField("_snapshot", BindingFlags.Static | BindingFlags.NonPublic);
+
+ private readonly object _savedSnapshot = _snapshotField.GetValue(null);
+
+ public void Dispose() => ManualAllowlist.LoadForTesting((BlocklistSnapshot)_savedSnapshot);
+
private static readonly IPAddress _listed = IPAddress.Parse("192.0.2.10");
private static readonly IPAddress _unlisted = IPAddress.Parse("192.0.2.11");
diff --git a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFileTests.cs b/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFileTests.cs
index 1e1c0fedf..991e49451 100644
--- a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFileTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFileTests.cs
@@ -21,14 +21,13 @@ using Xunit;
namespace Server.Tests.Network.Bans.Blocklist;
-public class BlocklistFileTests
+public class BlocklistFileTests : IDisposable
{
- private static string WriteTemp(string content)
- {
- var p = Path.Combine(Path.GetTempPath(), "bl-" + Guid.NewGuid().ToString("N") + ".txt");
- File.WriteAllText(p, content);
- return p;
- }
+ private readonly TempDirectory _temp = new("bl");
+
+ public void Dispose() => _temp.Dispose();
+
+ private string WriteTemp(string content) => _temp.WriteFile("blocklist.txt", content);
[Fact]
public void Reads_header_generated_and_count()
@@ -38,13 +37,12 @@ public class BlocklistFileTests
Assert.True(h.Present);
Assert.Equal("2026-07-21T09:24:23Z", h.Generated);
Assert.Equal(2, h.Count);
- File.Delete(p);
}
[Fact]
public void Missing_file_reports_absent_and_loads_empty()
{
- var p = Path.Combine(Path.GetTempPath(), "does-not-exist-" + Guid.NewGuid().ToString("N"));
+ var p = _temp.Combine("does-not-exist.txt");
Assert.False(BlocklistFile.TryReadHeader(p, out var h));
Assert.False(h.Present);
var snap = BlocklistFile.Load(p, out _, out _);
@@ -70,6 +68,5 @@ public class BlocklistFileTests
Assert.Equal(0, skipped);
Assert.True(snap.IsBanned(IPAddress.Parse("2.181.183.77")));
Assert.True(snap.IsBanned(IPAddress.Parse("223.169.4.9")));
- File.Delete(p);
}
}
diff --git a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFilterTests.cs b/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFilterTests.cs
index 4f3ee5ad1..6e630ba49 100644
--- a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFilterTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistFilterTests.cs
@@ -20,8 +20,10 @@ using Xunit;
namespace Server.Tests.Network.Bans.Blocklist;
-// No [Collection] and no static reset hook: the filter is an instance, so each test owns its own
-// snapshot and promote-guard. That is the point of it no longer being a static class.
+// The filter is an instance, so each test owns its own snapshot and promote-guard. Evaluate still
+// consults the process-wide ManualAllowlist and LoginAllowlist, which other tests rewrite, so this
+// runs in the sequential collection.
+[Collection("Sequential UOContent Tests")]
public class BlocklistFilterTests
{
private static BlocklistFilter WithList(string list, bool reportHits = true, long suppressionMs = 5000)
diff --git a/Projects/UOContent.Tests/Tests/Network/Bans/CrowdSecReporterTests.cs b/Projects/UOContent.Tests/Tests/Network/Bans/CrowdSecReporterTests.cs
index 6ad850f34..0a6d956d6 100644
--- a/Projects/UOContent.Tests/Tests/Network/Bans/CrowdSecReporterTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Bans/CrowdSecReporterTests.cs
@@ -218,14 +218,18 @@ public class CrowdSecReporterTests
///
/// The drain task must track the loop's lifetime, not just its first await — a ValueTask-returning
- /// drain loop passed to Task.Run yields a Task<ValueTask> that completes immediately, which makes
- /// Stop()'s drain-exited handshake a no-op. With an empty queue the loop parks on WaitToReadAsync,
- /// so a correctly unwrapped task cannot win this race; a slow pool only under-detects.
+ /// drain loop passed to Task.Run yields a Task<ValueTask> that completes at the first await
+ /// that suspends, which makes Stop()'s drain-exited handshake a no-op. The flush delay always
+ /// suspends before the first post, so once the client sees a post a wrapped task is already done.
///
[Fact]
public async Task Start_DrainTaskSpansLoopLifetime_NotJustTheFirstAwait()
{
- var reporter = new CrowdSecReporter(new NullAlertClient(), Settings());
+ var settings = Settings();
+ settings.FlushInterval = TimeSpan.FromMilliseconds(1);
+
+ var client = new SignalingAlertClient();
+ var reporter = new CrowdSecReporter(client, settings);
using var cts = new CancellationTokenSource();
reporter.Start(cts.Token);
@@ -233,14 +237,32 @@ public class CrowdSecReporterTests
var drain = reporter.DrainTaskForTesting;
Assert.NotNull(drain);
- var first = await Task.WhenAny(drain, Task.Delay(TimeSpan.FromMilliseconds(500)));
- Assert.False(ReferenceEquals(first, drain), "drain task completed while the loop was still running");
+ reporter.Report(IPAddress.Parse("9.9.9.9"), TimeSpan.FromHours(1), "rate-limit");
+ await client.Posted.Task.WaitAsync(TimeSpan.FromSeconds(10));
+
+ Assert.False(drain.IsCompleted, "drain task completed while the loop was still running");
reporter.Stop();
Assert.True(drain.IsCompleted, "Stop() returned before the drain loop exited");
}
+ private sealed class SignalingAlertClient : ICrowdSecAlertClient
+ {
+ public TaskCompletionSource Posted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ public ValueTask PostAlertsAsync(IReadOnlyList alerts, CancellationToken token)
+ {
+ Posted.TrySetResult();
+ return ValueTask.CompletedTask;
+ }
+
+ public ValueTask DeleteDecisionsAsync(string origin, IPAddress ip, CancellationToken token) =>
+ ValueTask.CompletedTask;
+
+ public void Dispose() { }
+ }
+
private sealed class NullAlertClient : ICrowdSecAlertClient
{
public ValueTask PostAlertsAsync(IReadOnlyList alerts, CancellationToken token) =>
diff --git a/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallPersistenceTests.cs b/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallPersistenceTests.cs
index 01691e223..c5617c0d3 100644
--- a/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallPersistenceTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallPersistenceTests.cs
@@ -21,8 +21,11 @@ using Xunit;
namespace Server.Tests.Network.Firewall;
[Collection("Sequential UOContent Tests")]
-public class FirewallPersistenceTests
+public class FirewallPersistenceTests : IDisposable
{
+ // The test host never configures the firewall, so empty is the state every other test expects.
+ public void Dispose() => Server.Network.Firewall.ResetForTesting();
+
[Fact]
public void ToSettings_RoundTrips_PermanentAndTtl()
{
diff --git a/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallTests.cs b/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallTests.cs
index 26e289f0c..7dba7508c 100644
--- a/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Firewall/FirewallTests.cs
@@ -21,8 +21,11 @@ using Xunit;
namespace Server.Tests.Network.Firewall;
[Collection("Sequential UOContent Tests")]
-public class FirewallTests
+public class FirewallTests : IDisposable
{
+ // The test host never configures the firewall, so empty is the state every other test expects.
+ public void Dispose() => Server.Network.Firewall.ResetForTesting();
+
private static IPAddress Ip(string s) => IPAddress.Parse(s);
[Fact]
diff --git a/Projects/UOContent.Tests/Tests/Network/LoginAllowlist/LoginAllowlistTests.cs b/Projects/UOContent.Tests/Tests/Network/LoginAllowlist/LoginAllowlistTests.cs
index 7f3780e58..b48933ec4 100644
--- a/Projects/UOContent.Tests/Tests/Network/LoginAllowlist/LoginAllowlistTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/LoginAllowlist/LoginAllowlistTests.cs
@@ -13,17 +13,74 @@
* along with this program. If not, see . *
*************************************************************************/
+using System;
+using System.Collections;
+using System.Collections.Generic;
using System.Net;
+using System.Reflection;
using Server.Network;
using Server.Network.Bans;
using Xunit;
namespace Server.Tests.Network.LoginAllowlists;
-// The list is static, so every test resets it first. Addresses come from TEST-NET-3 (203.0.113.0/24) so they
-// cannot collide with the blocklist tests if xUnit runs the two classes at the same time.
-public class LoginAllowlistTests
+// The list is static and the login path and BlocklistFilter both read it, so every test resets it first,
+// the class runs in the sequential collection, and Dispose puts the original state back. Addresses come
+// from TEST-NET-3 (203.0.113.0/24).
+[Collection("Sequential UOContent Tests")]
+public class LoginAllowlistTests : IDisposable
{
+ private readonly List<(FieldInfo Field, object Value, DictionaryEntry[] Entries)> _saved = [];
+
+ public LoginAllowlistTests()
+ {
+ var fields = typeof(LoginAllowlist).GetFields(BindingFlags.Static | BindingFlags.NonPublic);
+ for (var i = 0; i < fields.Length; i++)
+ {
+ var field = fields[i];
+ if (field.IsLiteral)
+ {
+ continue;
+ }
+
+ var value = field.GetValue(null);
+
+ // Readonly tables cannot be reassigned, so their contents are copied and refilled instead.
+ DictionaryEntry[] entries = null;
+ if (field.IsInitOnly && value is IDictionary table)
+ {
+ entries = new DictionaryEntry[table.Count];
+ table.CopyTo(entries, 0);
+ }
+ else if (field.IsInitOnly)
+ {
+ continue;
+ }
+
+ _saved.Add((field, value, entries));
+ }
+ }
+
+ public void Dispose()
+ {
+ for (var i = 0; i < _saved.Count; i++)
+ {
+ var (field, value, entries) = _saved[i];
+ if (entries == null)
+ {
+ field.SetValue(null, value);
+ continue;
+ }
+
+ var table = (IDictionary)value;
+ table.Clear();
+ for (var j = 0; j < entries.Length; j++)
+ {
+ table.Add(entries[j].Key, entries[j].Value);
+ }
+ }
+ }
+
private const long Ttl = 90 * 24 * 60 * 60; // 90 days, the shipped default
private const long Window = 3600;
private const long Now = 1_800_000_000;
diff --git a/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs b/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs
index 2fb0f73cb..062fba315 100644
--- a/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs
+++ b/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Net;
using Server.Accounting;
using Server.Accounting.Security;
@@ -22,14 +23,43 @@ public class AuthIdTests : IDisposable
IncomingAccountPackets.ClearAuthIdWindow();
}
+ // The constructor registers each account in the process-wide Accounts table.
+ private readonly List _created = [];
+
public void Dispose()
{
+ for (var i = 0; i < _created.Count; i++)
+ {
+ _created[i].Delete();
+ }
+
IncomingAccountPackets.ClearAuthIdWindow();
AccountSecurity.CurrentAlgorithm = _originalAlgorithm;
}
- private static IAccount CreateAccount(string username) =>
- Accounts.GetAccount(username) ?? new Account(username, "hunter2");
+ // Auth ids never read the password, so these accounts skip the Argon2 cost.
+ private IAccount CreateAccount(string username)
+ {
+ AccountSecurity.CurrentAlgorithm = PasswordProtectionAlgorithm.SHA2;
+ try
+ {
+ return TrackNewAccount(username);
+ }
+ finally
+ {
+ AccountSecurity.CurrentAlgorithm = PasswordProtectionAlgorithm.Argon2;
+ }
+ }
+
+ // The game-login tests run the real password check, so they keep the configured algorithm.
+ private IAccount CreateArgon2Account(string username) => TrackNewAccount(username);
+
+ private Account TrackNewAccount(string username)
+ {
+ var account = new Account(username, "hunter2");
+ _created.Add(account);
+ return account;
+ }
private static int Register(IAccount account, IPAddress address) =>
IncomingAccountPackets.RegisterAuthId(account, address, new ClientVersion(7, 0, 0, 0));
@@ -336,7 +366,7 @@ public class AuthIdTests : IDisposable
[Fact]
public void PreAuthenticatedGameLogin_SkipsThePasswordCheck()
{
- var account = CreateAccount("authid-preauth-user");
+ var account = CreateArgon2Account("authid-preauth-user");
using var ns = PacketTestUtilities.CreateTestNetState();
// A wrong password is accepted only because the auth id already vouched for the account.
@@ -349,7 +379,7 @@ public class AuthIdTests : IDisposable
[Fact]
public void GameLoginWithoutPreAuthentication_StillChecksThePassword()
{
- var account = CreateAccount("authid-nopreauth-user");
+ var account = CreateArgon2Account("authid-nopreauth-user");
using var ns = PacketTestUtilities.CreateTestNetState();
var wrong = new GameServer.GameLoginEventArgs(ns, account.Username, "wrong-password", false);
diff --git a/Projects/UOContent.Tests/Tests/WorldSaves/ArchiveJournalTests.cs b/Projects/UOContent.Tests/Tests/WorldSaves/ArchiveJournalTests.cs
index 5cf7dbeea..14db8d19d 100644
--- a/Projects/UOContent.Tests/Tests/WorldSaves/ArchiveJournalTests.cs
+++ b/Projects/UOContent.Tests/Tests/WorldSaves/ArchiveJournalTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Reflection;
using Server.Saves;
using Xunit;
@@ -9,16 +10,32 @@ namespace Server.Tests;
[Collection("Sequential UOContent Tests")]
public class ArchiveJournalTests : IDisposable
{
+ private static readonly FieldInfo _dataField =
+ typeof(ArchiveJournal).GetField("_data", BindingFlags.Static | BindingFlags.NonPublic);
+
+ private static readonly FieldInfo _journalPathField =
+ typeof(ArchiveJournal).GetField("_journalPath", BindingFlags.Static | BindingFlags.NonPublic);
+
+ private readonly object _savedData;
+ private readonly object _savedJournalPath;
private readonly string _testDir;
public ArchiveJournalTests()
{
+ // Configure() repoints the process-wide journal at the temp dir; restore it so no later
+ // caller writes into a deleted directory.
+ _savedData = _dataField.GetValue(null);
+ _savedJournalPath = _journalPathField.GetValue(null);
+
_testDir = Path.Combine(Path.GetTempPath(), $"modernuo-journal-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_testDir);
}
public void Dispose()
{
+ _dataField.SetValue(null, _savedData);
+ _journalPathField.SetValue(null, _savedJournalPath);
+
if (Directory.Exists(_testDir))
{
Directory.Delete(_testDir, true);