Merge branch 'main' into Refactor/MagerySpell.cs

This commit is contained in:
Bohica 2025-05-14 01:40:24 -07:00 committed by GitHub
commit 26b38ef924
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
284 changed files with 6151 additions and 4163 deletions

View file

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"modernuoschemagenerator": {
"version": "2.12.18",
"version": "2.12.20",
"commands": [
"ModernUOSchemaGenerator"
]

View file

@ -81,7 +81,7 @@ jobs:
run: dnf makecache --refresh && dnf install -y findutils libicu libdeflate-devel zstd libargon2-devel
if: ${{ matrix.packageManager == 'dnf' }}
- name: Install Prerequisites using apt
run: apt-get update -y && apt-get install -y curl libicu-dev libdeflate-dev zstd libargon2-dev
run: apt-get update -y && apt-get install -y curl libicu-dev libdeflate-dev zstd libargon2-dev tzdata
if: ${{ matrix.packageManager == 'apt' }}
- uses: actions/checkout@v4
with:

View file

@ -19,6 +19,6 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
fetch-depth: 0 # a full history is required for pull request analysis
- name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2024.1
uses: JetBrains/qodana-action@v2025.1
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}

Binary file not shown.

Binary file not shown.

View file

@ -15,12 +15,13 @@
using System;
using Serilog;
using Serilog.Core;
namespace Server.Logging;
public static class LogFactory
{
private static readonly Serilog.ILogger serilogLogger = new LoggerConfiguration()
private static readonly Logger serilogLogger = new LoggerConfiguration()
.WriteTo.Async(a => a.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}"
))

View file

@ -1,12 +1,13 @@
using System;
using System.Reflection;
using Xunit;
namespace Server.Tests;
internal class ServerFixture : IDisposable
[CollectionDefinition("Sequential Server Tests", DisableParallelization = true)]
public class ServerFixture : ICollectionFixture<ServerFixture>, IDisposable
{
// Global setup
static ServerFixture()
public ServerFixture()
{
Core.ApplicationAssembly = Assembly.GetExecutingAssembly(); // Server.Tests.dll

View file

@ -1,34 +1,33 @@
namespace Server
namespace Server.Tests;
public static class TestMapDefinitions
{
public static class TestMapDefinitions
public static void ConfigureTestMapDefinitions()
{
public static void ConfigureTestMapDefinitions()
{
RegisterMap(0, 0, 0, 7168, 4096, 4, "Felucca", MapRules.FeluccaRules);
RegisterMap(1, 1, 1, 7168, 4096, 0, "Trammel", MapRules.TrammelRules);
RegisterMap(2, 2, 2, 2304, 1600, 1, "Ilshenar", MapRules.TrammelRules);
RegisterMap(3, 3, 3, 2560, 2048, 1, "Malas", MapRules.TrammelRules);
RegisterMap(4, 4, 4, 1448, 1448, 1, "Tokuno", MapRules.TrammelRules);
RegisterMap(5, 5, 5, 1280, 4096, 1, "TerMur", MapRules.TrammelRules);
RegisterMap(0, 0, 0, 7168, 4096, 4, "Felucca", MapRules.FeluccaRules);
RegisterMap(1, 1, 1, 7168, 4096, 0, "Trammel", MapRules.TrammelRules);
RegisterMap(2, 2, 2, 2304, 1600, 1, "Ilshenar", MapRules.TrammelRules);
RegisterMap(3, 3, 3, 2560, 2048, 1, "Malas", MapRules.TrammelRules);
RegisterMap(4, 4, 4, 1448, 1448, 1, "Tokuno", MapRules.TrammelRules);
RegisterMap(5, 5, 5, 1280, 4096, 1, "TerMur", MapRules.TrammelRules);
RegisterMap(0x7F, 0x7F, 0x7F, Map.SectorSize, Map.SectorSize, 1, "Internal", MapRules.Internal);
}
RegisterMap(0x7F, 0x7F, 0x7F, Map.SectorSize, Map.SectorSize, 1, "Internal", MapRules.Internal);
}
private static void RegisterMap(
int mapIndex,
int mapID,
int fileIndex,
int width,
int height,
int season,
string name,
MapRules rules
)
{
var newMap = new Map(mapID, mapIndex, fileIndex, width, height, season, name, rules);
private static void RegisterMap(
int mapIndex,
int mapID,
int fileIndex,
int width,
int height,
int season,
string name,
MapRules rules
)
{
var newMap = new Map(mapID, mapIndex, fileIndex, width, height, season, name, rules);
Map.Maps[mapIndex] = newMap;
Map.AllMaps.Add(newMap);
}
Map.Maps[mapIndex] = newMap;
Map.AllMaps.Add(newMap);
}
}

View file

@ -8,7 +8,7 @@ public static class GumpUtilities
{
public static Packet Compile(this Gump g, NetState ns = null)
{
IGumpWriter disp = new DisplayGumpPacked(g);
var disp = new DisplayGumpPacked(g);
if (!g.Draggable)
{
@ -46,7 +46,7 @@ public static class GumpUtilities
disp.Flush();
return (Packet)disp;
return disp;
}
public static int Intern(this List<string> strings, string value)

View file

@ -1,9 +0,0 @@
using Xunit;
namespace Server.Tests
{
[CollectionDefinition("Sequential Tests", DisableParallelization = true)]
public class SequentialTestCollectionDefinition
{
}
}

View file

@ -5,9 +5,9 @@
<RootNamespace>Server.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -4,7 +4,7 @@ using Xunit;
namespace Server.Tests.Tests.Buffers;
[Collection("Sequential Tests")]
[Collection("Sequential Server Tests")]
public class STArrayPoolTests
{
[Theory]

View file

@ -3,7 +3,7 @@ using Xunit;
namespace Server.Tests.Buffers;
[Collection("Sequential Tests")]
[Collection("Sequential Server Tests")]
public class ValueStringBuilderTests
{
[Theory]

View file

@ -2,7 +2,7 @@ using Xunit;
namespace Server.Tests.Network;
[Collection("Sequential Tests")]
[Collection("Sequential Server Tests")]
public class ClientVersionTests
{
[Theory]

View file

@ -3,7 +3,8 @@ using Xunit;
namespace Server.Tests;
public sealed class WorldLocationTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public sealed class WorldLocationTests
{
private static Map CreateMap(string name) => new(0, 0, 0, 1, 1, 0, name, MapRules.Internal);

View file

@ -5,7 +5,8 @@ using Xunit;
namespace Server.Tests;
public class ContainerTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class ContainerTests
{
[Fact]
public void TestFindItemsByType()

View file

@ -2,7 +2,7 @@ using Xunit;
namespace Server.Tests;
[Collection("Sequential Tests")]
[Collection("Sequential Server Tests")]
public class LocalizationEntryTests
{
[Fact]

View file

@ -0,0 +1,137 @@
using System.Net;
using System.Threading.Tasks;
using Server.Network;
using Xunit;
namespace Server.Tests;
public class FirewallTests
{
[Fact]
public void Firewall_BlocksIPAddress_WhenAdded()
{
var ip = IPAddress.Parse("192.168.1.1");
var entry = new SingleIpFirewallEntry("192.168.1.1");
Assert.False(Firewall.IsBlocked(ip));
Firewall.Add(entry);
Assert.True(Firewall.IsBlocked(ip));
}
[Fact]
public void Firewall_DoesNotBlockIPAddress_WhenNotAdded()
{
var ip = IPAddress.Parse("192.168.1.2");
Assert.False(Firewall.IsBlocked(ip));
}
[Fact]
public void Firewall_StopsBlockingIPAddress_WhenRemoved()
{
var ip = IPAddress.Parse("192.168.1.3");
var entry = new SingleIpFirewallEntry("192.168.1.3");
Firewall.Add(entry);
Assert.True(Firewall.IsBlocked(ip));
Firewall.Remove(entry);
Assert.False(Firewall.IsBlocked(ip));
}
[Fact]
public void Firewall_BlocksIPRange()
{
var entry = new CidrFirewallEntry(IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.0.5"));
Firewall.Add(entry);
Assert.True(Firewall.IsBlocked(IPAddress.Parse("10.0.0.1")));
Assert.True(Firewall.IsBlocked(IPAddress.Parse("10.0.0.3")));
Assert.True(Firewall.IsBlocked(IPAddress.Parse("10.0.0.5")));
Assert.False(Firewall.IsBlocked(IPAddress.Parse("10.0.0.6")));
}
[Fact]
public void Firewall_CacheInvalidation_WorksOnUpdate()
{
var ip = IPAddress.Parse("192.168.1.10");
var entry = new SingleIpFirewallEntry("192.168.1.10");
Firewall.Add(entry);
Assert.True(Firewall.IsBlocked(ip));
Firewall.Remove(entry);
Assert.False(Firewall.IsBlocked(ip));
}
[Fact]
public void Firewall_ReadsFirewallSetCorrectly()
{
var entry = new SingleIpFirewallEntry("172.16.0.1");
Firewall.Add(entry);
bool found = false;
Firewall.ReadFirewallSet(set =>
{
found = set.Contains(entry);
});
Assert.True(found);
}
[Fact]
public void Firewall_IsThreadSafe()
{
IPAddress[] testIps = new IPAddress[256];
for (int i = 0; i <= 255; i++)
{
testIps[i] = IPAddress.Parse($"192.168.0.{i}");
}
var entry = new CidrFirewallEntry(IPAddress.Parse("192.168.0.1"), IPAddress.Parse("192.168.0.255"));
Firewall.Add(entry);
Parallel.ForEach(testIps, ip =>
{
bool shouldBlock = int.Parse(ip.ToString().Split('.')[3]) is > 0;
Assert.Equal(shouldBlock, Firewall.IsBlocked(ip));
});
Firewall.Remove(entry);
Parallel.ForEach(testIps, ip =>
{
Assert.False(Firewall.IsBlocked(ip));
});
}
[Fact]
public void Firewall_DoesNotThrowWhenRemovingNonExistentEntry()
{
var entry = new SingleIpFirewallEntry("203.0.113.5");
Assert.False(Firewall.Remove(entry));
}
[Fact]
public void Firewall_CacheHandlesMultipleUpdates()
{
var ip = IPAddress.Parse("192.168.1.20");
var entry = new SingleIpFirewallEntry("192.168.1.20");
Firewall.Add(entry);
Assert.True(Firewall.IsBlocked(ip));
Firewall.Remove(entry);
Assert.False(Firewall.IsBlocked(ip));
Firewall.Add(entry);
Assert.True(Firewall.IsBlocked(ip));
Firewall.Remove(entry);
Assert.False(Firewall.IsBlocked(ip));
}
}

View file

@ -7,7 +7,8 @@ using Xunit;
namespace Server.Tests.Network;
public class AccountPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class AccountPacketTests
{
private class MockedAccount : IAccount
{

View file

@ -4,8 +4,8 @@ using Xunit;
namespace Server.Tests.Network
{
[Collection("Sequential Tests")]
public class ContainerPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class ContainerPacketTests
{
[Fact]

View file

@ -3,7 +3,8 @@ using Xunit;
namespace Server.Tests.Network
{
public class DamagePacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class DamagePacketTests
{
[Theory, InlineData(10), InlineData(-5), InlineData(1024)]
public void TestDamagePacketOld(int inputAmount)

View file

@ -4,7 +4,8 @@ using Xunit;
namespace Server.Tests.Network
{
public class EquipmentPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class EquipmentPacketTests
{
[Theory]
[InlineData(null, false)]

View file

@ -3,8 +3,8 @@ using Xunit;
namespace Server.Tests.Network;
[Collection("Sequential Tests")]
public class GumpPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class GumpPacketTests
{
[Theory]
[InlineData(100, 10)]

View file

@ -3,7 +3,8 @@ using Xunit;
namespace Server.Tests.Network
{
public class ItemPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class ItemPacketTests
{
[Fact]
public void TestWorldItemPacket()

View file

@ -3,7 +3,8 @@ using Xunit;
namespace Server.Tests.Network
{
public class MapPatchesTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class MapPatchesTests
{
[Fact]
public void TestMapPatches()

View file

@ -27,8 +27,8 @@ namespace Server.Tests.Network
}
}
[Collection("Sequential Tests")]
public class MenuPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class MenuPacketTests
{
[Fact]
public void TestDisplayItemListMenu()

View file

@ -3,8 +3,8 @@ using Xunit;
namespace Server.Tests.Network;
[Collection("Sequential Tests")]
public class MobilePacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class MobilePacketTests
{
[Fact]
public void TestDeathAnimation()

View file

@ -3,7 +3,8 @@ using Xunit;
namespace Server.Tests.Network
{
public class MovementPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class MovementPacketTests
{
[Theory]
[InlineData(0)]

View file

@ -5,8 +5,8 @@ using Xunit;
namespace Server.Tests.Network
{
[Collection("Sequential Tests")]
public class PlayerPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class PlayerPacketTests
{
[Theory]
[InlineData(StatLockType.Down, StatLockType.Up, StatLockType.Locked)]

View file

@ -4,7 +4,8 @@ using Xunit;
namespace Server.Tests.Network
{
public class SecureTradePacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class SecureTradePacketTests
{
[Theory]
[InlineData("short-name")]

View file

@ -5,8 +5,8 @@ using Xunit;
namespace Server.Tests.Network
{
[Collection("Sequential Tests")]
public class VendorBuyPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class VendorBuyPacketTests
{
[Theory]
[InlineData(ProtocolChanges.None)]

View file

@ -5,8 +5,8 @@ using Xunit;
namespace Server.Tests.Network
{
[Collection("Sequential Tests")]
public class VendorSellPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class VendorSellPacketTests
{
[Fact]
public void TestVendorSellList()

View file

@ -2,40 +2,40 @@ using Server.Network;
using Server.Tests.Network;
using Xunit;
namespace Server.Tests
namespace Server.Tests;
[Collection("Sequential Server Tests")]
public class VirtualHairPacketTests
{
public class VirtualHairPacketTests: IClassFixture<ServerFixture>
[Fact]
public void TestSendVirtualHairUpdate()
{
[Fact]
public void TestSendVirtualHairUpdate()
{
var m = new Mobile((Serial)0x1024u);
m.DefaultMobileInit();
m.HairHue = 0x1000;
m.HairItemID = 0x2000;
var m = new Mobile((Serial)0x1024u);
m.DefaultMobileInit();
m.HairHue = 0x1000;
m.HairItemID = 0x2000;
var expected = new HairEquipUpdate(m).Compile();
var expected = new HairEquipUpdate(m).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendHairEquipUpdatePacket(m, (uint)m.Hair.VirtualSerial, m.Hair.ItemId, m.Hair.Hue, Layer.Hair);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendHairEquipUpdatePacket(m, (uint)m.Hair.VirtualSerial, m.Hair.ItemId, m.Hair.Hue, Layer.Hair);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
[Fact]
public void TestSendRemoveVirtualHair()
{
var m = new Mobile((Serial)0x1024u);
m.DefaultMobileInit();
[Fact]
public void TestSendRemoveVirtualHair()
{
var m = new Mobile((Serial)0x1024u);
m.DefaultMobileInit();
var expected = new RemoveHair(m).Compile();
var expected = new RemoveHair(m).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendRemoveHairPacket((uint) m.Hair.VirtualSerial);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendRemoveHairPacket((uint) m.Hair.VirtualSerial);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}

View file

@ -8,7 +8,8 @@ using Xunit;
namespace Server.Tests;
public class TypeConverterTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class TypeConverterTests
{
[Fact]
public void TestReadAfterWrite()

View file

@ -3,8 +3,8 @@ using Xunit;
namespace Server.Tests;
[Collection("Sequential Tests")]
public class TimerTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class TimerTests
{
[Theory]
[InlineData(0L, 8L)]

View file

@ -4,7 +4,7 @@ using Xunit;
namespace Server.Tests;
[Collection("Sequential Tests")]
[Collection("Sequential Server Tests")]
public class TestStringHelpers
{
[Theory]

View file

@ -2,8 +2,8 @@ using Xunit;
namespace Server.Tests;
[Collection("Sequential Tests")]
public class VirtualSerialTests : IClassFixture<ServerFixture>
[Collection("Sequential Server Tests")]
public class VirtualSerialTests
{
[Fact]
public void TestNewVirtualGetsAndRollover()

View file

@ -234,10 +234,10 @@ public class TypeCache
private static ILogger logger = LogFactory.GetLogger(typeof(TypeCache));
#endif
private Dictionary<ulong, Type[]> _nameMap = new();
private Dictionary<ulong, Type[]> _nameMapInsensitive = new();
private Dictionary<ulong, Type[]> _fullNameMap = new();
private Dictionary<ulong, Type[]> _fullNameMapInsensitive = new();
private readonly Dictionary<ulong, Type[]> _nameMap = [];
private readonly Dictionary<ulong, Type[]> _nameMapInsensitive = [];
private readonly Dictionary<ulong, Type[]> _fullNameMap = [];
private readonly Dictionary<ulong, Type[]> _fullNameMapInsensitive = [];
public TypeCache(Assembly asm)
{
@ -248,15 +248,6 @@ public class TypeCache
var fullNameMap = new Dictionary<string, HashSet<Type>>();
var fullNameMapInsensitive = new Dictionary<string, HashSet<Type>>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void addTypeToRefs(Type type, string typeName, string fullTypeName)
{
AddToRefs(type, typeName, nameMap);
AddToRefs(type, typeName.ToLower(), nameMapInsensitive);
AddToRefs(type, fullTypeName, fullNameMap);
AddToRefs(type, fullTypeName.ToLower(), fullNameMapInsensitive);
}
var aliasType = typeof(TypeAliasAttribute);
for (var i = 0; i < Types.Length; i++)
{
@ -267,7 +258,7 @@ public class TypeCache
for (var j = 0; j < alias.Aliases.Length; j++)
{
var fullTypeName = alias.Aliases[j];
var typeName = fullTypeName[(fullTypeName.LastIndexOf('.')+1)..];
var typeName = fullTypeName[(fullTypeName.AsSpan().LastIndexOf('.') + 1)..];
addTypeToRefs(current, typeName, fullTypeName);
}
}
@ -322,6 +313,17 @@ public class TypeCache
}
#endif
}
return;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void addTypeToRefs(Type type, string typeName, string fullTypeName)
{
AddToRefs(type, typeName, nameMap);
AddToRefs(type, typeName.ToLower(), nameMapInsensitive);
AddToRefs(type, fullTypeName, fullNameMap);
AddToRefs(type, fullTypeName.ToLower(), fullNameMapInsensitive);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -338,7 +340,7 @@ public class TypeCache
}
else
{
refs = new HashSet<Type> { type };
refs = [type];
map.Add(key, refs);
}
}
@ -369,12 +371,12 @@ public class TypeCache
if (ignoreCase)
{
var map = full ? cache._fullNameMapInsensitive : cache._nameMapInsensitive;
_values = map.TryGetValue(hash, out var values) ? values : Array.Empty<Type>();
_values = map.TryGetValue(hash, out var values) ? values : [];
}
else
{
var map = full ? cache._fullNameMap : cache._nameMap;
_values = map.TryGetValue(hash, out var values) ? values : Array.Empty<Type>();
_values = map.TryGetValue(hash, out var values) ? values : [];
}
_index = 0;

View file

@ -145,16 +145,18 @@ public class CommandPropertyAttribute : Attribute
AccessLevel level,
bool readOnly = false,
bool canModify = false
) : this(level, level)
) : this(level, level, readOnly, canModify)
{
ReadOnly = readOnly;
CanModify = canModify;
}
public CommandPropertyAttribute(AccessLevel readLevel, AccessLevel writeLevel)
public CommandPropertyAttribute(
AccessLevel readLevel, AccessLevel writeLevel, bool readOnly = false, bool canModify = false
)
{
ReadLevel = readLevel;
WriteLevel = writeLevel;
ReadOnly = readOnly;
CanModify = canModify;
}
public AccessLevel ReadLevel { get; }
@ -170,16 +172,18 @@ public class SerializedCommandPropertyAttribute : SerializedPropertyAttrAttribut
AccessLevel level,
bool readOnly = false,
bool canModify = false
) : this(level, level)
) : this(level, level, readOnly, canModify)
{
ReadOnly = readOnly;
CanModify = canModify;
}
public SerializedCommandPropertyAttribute(AccessLevel readLevel, AccessLevel writeLevel)
public SerializedCommandPropertyAttribute(
AccessLevel readLevel, AccessLevel writeLevel, bool readOnly = false, bool canModify = false
)
{
ReadLevel = readLevel;
WriteLevel = writeLevel;
ReadOnly = readOnly;
CanModify = canModify;
}
public AccessLevel ReadLevel { get; }

View file

@ -309,7 +309,7 @@ public ref struct ValueStringBuilder
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(ReadOnlySpan<char> value)
public void Append(scoped ReadOnlySpan<char> value)
{
int pos = _length;
if (pos > _chars.Length - value.Length)

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: ArtData.cs *
* *
@ -50,18 +50,18 @@ public class ArtData : IDisposable
}
}
public Rectangle2D GetStaticBounds(int index)
public (ushort Width, ushort Height, Rectangle2D Bounds) GetStaticBounds(int index)
{
if (index is < 0 or > 0x10000)
{
return Rectangle2D.Empty;
return (0, 0, Rectangle2D.Empty);
}
index += 16384;
if (!_dataRanges.TryGetValue(index, out var entry))
{
return Rectangle2D.Empty;
return (0, 0, Rectangle2D.Empty);
}
Span<ushort> buffer = stackalloc ushort[entry.Size / 2];
@ -73,10 +73,10 @@ public class ArtData : IDisposable
if (width == 0 || height == 0)
{
return Rectangle2D.Empty;
return (0, 0, Rectangle2D.Empty);
}
return GetBoundsFromRGBA1555Bitmap(width, height, buffer[4..]);
return (width, height, GetBoundsFromRGBA1555Bitmap(width, height, buffer[4..]));
}
private static Dictionary<int, UOPEntry> LoadMulRanges(string idxPath)

View file

@ -30,6 +30,7 @@ public class ClientVersion : IComparable<ClientVersion>, IComparer<ClientVersion
public static readonly ClientVersion Version6000 = new("6.0.0.0");
public static readonly ClientVersion Version6000KR = new("66.55.38"); // KR 2.44.0.15 (First release)
public static readonly ClientVersion Version6017 = new("6.0.1.7");
public static readonly ClientVersion Version6050 = new("6.0.5.0");
public static readonly ClientVersion Version60142 = new("6.0.14.2");
public static readonly ClientVersion Version60142KR = new("66.55.53"); // KR 2.59.0.2
public static readonly ClientVersion Version7000 = new("7.0.0.0");

View file

@ -30,6 +30,8 @@ public static class UOClient
public static CUOSettings CuoSettings { get; private set; }
public static ClientVersion ServerClientVersion { get; private set; }
public static ClientVersion MinRequired { get; set; }
public static ClientVersion MaxRequired { get; set; }
public static void Load()
{

View file

@ -0,0 +1,177 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: EntityFinalizationTracker.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Server.Logging;
namespace Server;
public static class EntityFinalizationTracker
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(EntityFinalizationTracker));
private sealed record TrackedEntity(WeakReference<IEntity> Reference, int ReferenceHash, DateTime RemovedAt);
private static readonly Lock _sync = new();
private static bool _enabled;
private static DateTime _nextCheck = DateTime.MinValue;
#if TRACK_LEAKS
private const bool CanBeEnabled = true;
#else
private const bool CanBeEnabled = false;
#endif
public static void Configure()
{
CommandSystem.Register("gc", AccessLevel.Administrator, _ => GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true));
CommandSystem.Register("TrackLeaks", AccessLevel.Developer, TrackLeaks_OnCommand);
}
[Usage("TrackLeaks <on|off>")]
[Description("Enables or disables entity leak tracking. May impact performance and should be used only when necessary!")]
private static void TrackLeaks_OnCommand(CommandEventArgs e)
{
var from = e.Mobile;
if (!CanBeEnabled) {
from.SendMessage("Entity leak tracking is not enabled in this build. Rebuild with TRACK_LEAKS defined.");
return;
}
if (e.Arguments.Length == 0)
{
from.SendMessage("Usage: TrackLeaks <on|off>");
return;
}
var enable = Utility.ToBoolean(e.Arguments[0]);
var status = enable ? "enabled" : "disabled";
if (enable == _enabled)
{
from.SendMessage($"Entity leak tracking already {status}.");
return;
}
if (_enabled)
{
EnableLeakTracking(from);
}
else
{
DisableLeakTracking(from);
}
}
private static void EnableLeakTracking(Mobile from)
{
if (_enabled)
{
return;
}
_enabled = true;
Gen2GcCallback.Register(() =>
{
CheckLeaks();
return _enabled;
});
from.SendMessage("Entity leak tracking enabled.");
logger.Warning("Entity leak tracking enabled by {Name} ({Serial:X8})", from.Name, from.Serial);
}
private static void DisableLeakTracking(Mobile from)
{
if (!_enabled)
{
return;
}
_enabled = false;
from.SendMessage("Entity leak tracking disabled.");
logger.Warning("Entity leak tracking disabled by {Name} ({Serial:X8})", from.Name, from.Serial);
}
private static readonly List<TrackedEntity> _entities = [];
private static readonly HashSet<int> _finalizedHashes = [];
public static void TrackEntity<T>(T entity) where T : IEntity
{
lock (_sync)
{
var hash = RuntimeHelpers.GetHashCode(entity);
_entities.Add(new TrackedEntity(new WeakReference<IEntity>(entity), hash, Core.Now));
}
}
public static void NotifyFinalized(object entity)
{
lock (_sync)
{
_finalizedHashes.Add(RuntimeHelpers.GetHashCode(entity));
}
}
private static void CheckLeaks()
{
if (_entities.Count == 0)
{
return;
}
var now = Core.Now;
if (now <= _nextCheck)
{
return;
}
_nextCheck = now + TimeSpan.FromMinutes(2);
_entities.RemoveAll(entry =>
{
if (_finalizedHashes.Remove(entry.ReferenceHash))
{
return true;
}
if (!entry.Reference.TryGetTarget(out var obj) || obj is not IEntity entity)
{
return true;
}
if (ExceptionToLeakCheck(entity))
{
return false;
}
var duration = now - entry.RemovedAt;
if (duration <= TimeSpan.FromSeconds(120))
{
return false;
}
logger.Warning("[Leak Warning] {Name} ({Serial:X8}) collected but not finalized after {Duration}.", entity.GetType().Name, entity.Serial, duration);
return false;
});
}
private static bool ExceptionToLeakCheck(IEntity entity) =>
// Mobiles that are deleted, but have a reference to a corpse that is not deleted should be exempt
(entity as Mobile)?.Corpse?.Deleted == false;
}

View file

@ -21,18 +21,29 @@ namespace Server;
public sealed class EventLoopContext : SynchronizationContext
{
private readonly ConcurrentQueue<Action> _queue;
private readonly Thread _mainThread;
public EventLoopContext()
public enum Priority
{
_queue = new ConcurrentQueue<Action>();
Normal,
High
}
private readonly ConcurrentQueue<Action> _queue;
private readonly ConcurrentQueue<Action> _priorityQueue;
private readonly Thread _mainThread;
private readonly int _maxPerFrame;
public EventLoopContext(int maxPerFrame = 128)
{
_maxPerFrame = maxPerFrame;
_queue = [];
_priorityQueue = [];
_mainThread = Thread.CurrentThread;
}
public override SynchronizationContext CreateCopy() => new EventLoopContext();
public void Post(Action d) => _queue.Enqueue(d);
public void Post(Action d, Priority priority = Priority.Normal) =>
(priority == Priority.High ? _priorityQueue : _queue).Enqueue(d);
public override void Post(SendOrPostCallback d, object state) => _queue.Enqueue(() => d(state));
@ -62,7 +73,17 @@ public sealed class EventLoopContext : SynchronizationContext
throw new Exception("Called EventLoop.ExecuteTasks on incorrect thread!");
}
var count = _queue.Count;
var count = _priorityQueue.Count;
for (int i = 0; i < count; i++)
{
if (_priorityQueue.TryDequeue(out var a))
{
a();
}
}
count = Math.Min(_queue.Count, _maxPerFrame);
for (int i = 0; i < count; i++)
{

View file

@ -13,18 +13,31 @@ namespace System;
/// </summary>
internal sealed class Gen2GcCallback : CriticalFinalizerObject
{
private readonly Func<object, bool> _callback;
private readonly Func<bool>? _callback0;
private readonly Func<object, bool>? _callback1;
private GCHandle _weakTargetObj;
private Gen2GcCallback(Func<bool> callback) => _callback0 = callback;
private Gen2GcCallback(Func<object, bool> callback, object targetObj)
{
_callback = callback;
_callback1 = callback;
_weakTargetObj = GCHandle.Alloc(targetObj, GCHandleType.Weak);
}
/// <summary>
/// Schedule 'callback' to be called in the next GC. If the callback returns true it is
/// rescheduled for the next Gen 2 GC. Otherwise the callbacks stop.
/// rescheduled for the next Gen 2 GC, otherwise the callback stops.
/// </summary>
public static void Register(Func<bool> callback)
{
// Create an unreachable object that remembers the callback function and target object.
new Gen2GcCallback(callback);
}
/// <summary>
/// Schedule 'callback' to be called in the next GC. If the callback returns true it is
/// rescheduled for the next Gen 2 GC, otherwise the callback stops.
///
/// NOTE: This callback will be kept alive until either the callback function returns false,
/// or the target object dies.
@ -51,8 +64,8 @@ internal sealed class Gen2GcCallback : CriticalFinalizerObject
// Execute the callback method.
try
{
Debug.Assert(_callback != null);
if (_callback?.Invoke(targetObj) != true)
Debug.Assert(_callback1 != null);
if (!_callback1(targetObj))
{
// If the callback returns false, this callback object is no longer needed.
_weakTargetObj.Free();
@ -63,8 +76,29 @@ internal sealed class Gen2GcCallback : CriticalFinalizerObject
{
// Ensure that we still get a chance to resurrect this object, even if the callback throws an exception.
#if DEBUG
// Except in DEBUG, as we really shouldn't be hitting any exceptions here.
throw;
// Except in DEBUG, as we really shouldn't be hitting any exceptions here.
throw;
#endif
}
}
else
{
// Execute the callback method.
try
{
Debug.Assert(_callback0 != null);
if (!_callback0())
{
// If the callback returns false, this callback object is no longer needed.
return;
}
}
catch
{
// Ensure that we still get a chance to resurrect this object, even if the callback throws an exception.
#if DEBUG
// Except in DEBUG, as we really shouldn't be hitting any exceptions here.
throw;
#endif
}
}

View file

@ -423,7 +423,7 @@ public partial class Container : Item
return false;
}
public virtual bool TryDropItems(Mobile from, bool sendFullMessage, params Item[] droppedItems)
public virtual bool TryDropItems(Mobile from, bool sendFullMessage, params ReadOnlySpan<Item> droppedItems)
{
var dropItems = new List<Item>();
var stackItems = new List<ItemStackEntry>();

View file

@ -328,7 +328,7 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
public virtual TimeSpan DecayTime => DefaultDecayTime;
[CommandProperty(AccessLevel.GameMaster)]
public virtual bool Decays => Movable && Visible && Spawner == null;
public virtual bool Decays => Movable && Visible && Spawner == null;
public DateTime LastMoved { get; set; }
@ -2346,7 +2346,7 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
};
}
var bounds = ItemBounds.Table[itemID & 0x3FFF];
var bounds = ItemBounds.Bounds[itemID & 0x3FFF];
if (doubled)
{
@ -3268,6 +3268,13 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
}
}
#if TRACK_LEAKS
~Item()
{
EntityFinalizationTracker.NotifyFinalized(this);
}
#endif
public virtual void OnDelete()
{
if (Spawner != null)

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: ItemBounds.cs *
* *
@ -17,13 +17,15 @@ using System;
using System.IO;
using System.Threading;
using Server.Logging;
using Size = System.ValueTuple<ushort, ushort>;
namespace Server;
public static class ItemBounds
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(ItemBounds));
private static readonly string _pathToBounds = Path.Combine(Core.BaseDirectory, "Data", "Binary", "Bounds.bin");
private const string _boundsFileName = "ItemBounds.bin";
private static readonly string _boundsFolder = Path.Combine(Core.BaseDirectory, "Data", "Items");
private static bool _isGenerating;
public static void Configure()
@ -32,7 +34,7 @@ public static class ItemBounds
}
[Usage("GenBounds")]
[Description("Asynchronously generates the bounds.bin file from art.mul/artidx.mul or artLegacyMUL.uop to determine container boundaries.")]
[Description("Asynchronously generates the ItemBounds.bin file from art.mul/artidx.mul or artLegacyMUL.uop to determine graphic sizes and boundaries.")]
private static void GenBounds_OnCommand(CommandEventArgs e)
{
GenerateBoundsFileAsync(e.Mobile);
@ -40,19 +42,20 @@ public static class ItemBounds
static ItemBounds()
{
Table = new Rectangle2D[TileData.ItemTable.Length];
if (!File.Exists(_pathToBounds))
if (!File.Exists(Path.Combine(_boundsFolder, _boundsFileName)))
{
logger.Information("Generating {BoundsFilePath}...", "Bounds.bin");
logger.Information("Generating {BoundsFilePath}...", _boundsFileName);
try
{
GenerateBoundsFile();
logger.Information("Generated {BoundsFilePath} successfully.", "Bounds.bin");
GenerateBoundsFile(out var sizes, out var bounds);
Sizes = sizes;
Bounds = bounds;
logger.Information("Generated {BoundsFilePath} successfully.", _boundsFileName);
}
catch (Exception ex)
{
logger.Error(ex, "Failed to generate {BoundsFilePath}", "Bounds.bin");
logger.Error(ex, "Failed to generate {BoundsFilePath}", _boundsFileName);
}
return;
@ -61,7 +64,9 @@ public static class ItemBounds
GenerateTable();
}
public static Rectangle2D[] Table { get; private set; }
public static Size[] Sizes { get; private set; }
public static Rectangle2D[] Bounds { get; private set; }
private static void GenerateBoundsFileAsync(Mobile m)
{
@ -76,16 +81,21 @@ public static class ItemBounds
state =>
{
var from = state as Mobile;
logger.Information("Generating {BoundsFilePath}...", "Bounds.bin");
logger.Information("Generating {BoundsFilePath}...", _boundsFileName);
if (from != null)
{
Core.LoopContext.Post(() => from?.SendMessage("Generating bounds file..."));
Core.LoopContext.Post(() => from.SendMessage("Generating bounds file..."));
}
try
{
var table = GenerateBoundsFile();
Core.LoopContext.Post(() => Table = table);
GenerateBoundsFile(out var sizes, out var bounds);
Core.LoopContext.Post(() =>
{
Sizes = sizes;
Bounds = bounds;
}
);
}
catch (Exception ex)
{
@ -93,21 +103,21 @@ public static class ItemBounds
{
Core.LoopContext.Post(() =>
{
from?.SendMessage("Failed to generate bounds file:");
from?.SendMessage(ex.Message);
from.SendMessage("Failed to generate bounds file:");
from.SendMessage(ex.Message);
}
);
}
logger.Error(ex, "Failed to generate {BoundsFilePath}", "Bounds.bin");
logger.Error(ex, "Failed to generate {BoundsFilePath}", _boundsFileName);
return;
}
if (from != null)
{
Core.LoopContext.Post(
() => from?.SendMessage(
$"Bounds file saved to {Path.GetRelativePath(Core.BaseDirectory, _pathToBounds)}."
() => from.SendMessage(
$"Bounds file saved to {Path.GetRelativePath(Core.BaseDirectory, Path.Combine(_boundsFolder, _boundsFileName))}."
)
);
}
@ -119,48 +129,57 @@ public static class ItemBounds
);
}
private static Rectangle2D[] GenerateBoundsFile()
private static void GenerateBoundsFile(out Size[] sizes, out Rectangle2D[] bounds)
{
var table = new Rectangle2D[TileData.ItemTable.Length];
bounds = new Rectangle2D[TileData.ItemTable.Length];
sizes = new Size[TileData.ItemTable.Length];
using var artData = new ArtData();
if (!artData.IsInitialized)
{
throw new FileNotFoundException("Unable to load art.mul/artidx.mul or artLegacyMUL.uop");
}
using var fs = new FileStream(_pathToBounds, FileMode.Create, FileAccess.Write);
PathUtility.EnsureDirectory(_boundsFolder);
using var fs = new FileStream(Path.Combine(_boundsFolder, _boundsFileName), FileMode.Create, FileAccess.Write);
using var bw = new BinaryWriter(fs);
for (var i = 0; i < table.Length; i++)
for (var i = 0; i < bounds.Length; i++)
{
var bounds = artData.GetStaticBounds(i);
var (w, h, b) = artData.GetStaticBounds(i);
bw.Write((short)bounds.X);
bw.Write((short)bounds.Y);
bw.Write((short)(bounds.X + bounds.Width + 1));
bw.Write((short)(bounds.Y + bounds.Height + 1));
bw.Write(w);
bw.Write(h);
bw.Write((short)b.X);
bw.Write((short)b.Y);
bw.Write((short)(b.X + b.Width + 1));
bw.Write((short)(b.Y + b.Height + 1));
table[i] = bounds;
bounds[i].Set(b.X, b.Y, b.Width, b.Height);
sizes[i] = (w, h);
}
return table;
}
private static void GenerateTable()
{
using var fs = new FileStream(_pathToBounds, FileMode.Open, FileAccess.Read, FileShare.Read);
Bounds = new Rectangle2D[TileData.ItemTable.Length];
Sizes = new Size[TileData.ItemTable.Length];
using var fs = new FileStream(Path.Combine(_boundsFolder, _boundsFileName), FileMode.Open, FileAccess.Read, FileShare.Read);
using var bin = new BinaryReader(fs);
var count = Math.Min(Table.Length, (int)(fs.Length / 8));
var count = Math.Min(Bounds.Length, (int)(fs.Length / 8));
for (var i = 0; i < count; ++i)
{
Sizes[i] = (bin.ReadUInt16(), bin.ReadUInt16());
int xMin = bin.ReadInt16();
int yMin = bin.ReadInt16();
int xMax = bin.ReadInt16();
int yMax = bin.ReadInt16();
Table[i].Set(xMin, yMin, xMax - xMin, yMax - yMin);
Bounds[i].Set(xMin, yMin, xMax - xMin, yMax - yMin);
}
}
}

View file

@ -102,7 +102,8 @@ public static class Core
private static long _tickCount;
private static DateTime _now;
// Make this available to unit tests for mocking
internal static DateTime _now;
public static long TickCount => _tickCount;
@ -346,8 +347,20 @@ public static class Core
}
}
private static readonly bool UseFastTimestampMath = Stopwatch.Frequency % 1000 == 0;
private static readonly ulong FrequencyInMilliseconds = (ulong)Stopwatch.Frequency / 1000;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long GetTimestamp() => 1000L * Stopwatch.GetTimestamp() / Stopwatch.Frequency;
public static long GetTimestamp()
{
if (UseFastTimestampMath)
{
return (long)((ulong)Stopwatch.GetTimestamp() / FrequencyInMilliseconds);
}
// Fast calculation will be lossy, fallback to slower but accurate calculation
return (long)((UInt128)Stopwatch.GetTimestamp() * 1000 / (ulong)Stopwatch.Frequency);
}
public static void Setup(Assembly applicationAssembly, Process process)
{

View file

@ -25,10 +25,10 @@ using Server.Mobiles;
using Server.Network;
using Server.Prompts;
using Server.Targeting;
using Server.Text;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Server.Buffers;
using CalcMoves = Server.Movement.Movement;
namespace Server;
@ -895,7 +895,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
[CommandProperty(AccessLevel.Administrator)]
public bool AutoPageNotify { get; set; }
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Owner)]
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator, canModify: true)]
public IAccount Account { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
@ -4573,6 +4573,13 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
}
}
#if TRACK_LEAKS
~Mobile()
{
EntityFinalizationTracker.NotifyFinalized(this);
}
#endif
/// <summary>
/// Overridable. Virtual event invoked before the Mobile is deleted.
/// </summary>
@ -5371,13 +5378,32 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
return false;
}
using var sb = new ValueStringBuilder(stackalloc char[Math.Min(text.Length, 256)]);
for (var i = 0; i < text.Length; ++i)
ReadOnlySpan<char> ghostChars = (GhostChars ?? DefaultGhostChars).AsSpan();
var length = text.Length;
char[] rentedChars = null;
Span<char> chars = length <= 256
? stackalloc char[length]
: rentedChars = STArrayPool<char>.Shared.Rent(length);
try
{
sb.Append(text[i] != ' ' ? (GhostChars ?? DefaultGhostChars).RandomElement() : ' ');
var textSpan = text.AsSpan();
for (var i = 0; i < textSpan.Length; ++i)
{
chars[i] = textSpan[i] != ' ' ? ghostChars.RandomElement() : ' ';
}
text = new string(chars[..length]);
}
finally
{
if (rentedChars != null)
{
STArrayPool<char>.Shared.Return(rentedChars);
}
}
text = sb.ToString();
context = m_GhostMutateContext;
return true;
}
@ -5418,7 +5444,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
public virtual bool CheckHearsMutatedSpeech(Mobile m, object context) =>
context != m_GhostMutateContext || m.Alive && !m.CanHearGhosts;
private void AddSpeechItemsFrom(List<IEntity> list, Container cont)
private static void AddSpeechItemsFrom(List<IEntity> list, Container cont)
{
for (var i = 0; i < cont.Items.Count; ++i)
{
@ -5470,33 +5496,12 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
break;
}
case MessageType.System:
{
break;
}
case MessageType.Label:
{
break;
}
case MessageType.Focus:
{
break;
}
case MessageType.Spell:
{
break;
}
case MessageType.Guild:
{
break;
}
case MessageType.Alliance:
{
break;
}
case MessageType.Command:
{
break;
}
case MessageType.Encoded:
{
break;
@ -7988,7 +7993,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
public static TimeSpan GetManaRegenRate(Mobile m) => ManaRegenRateHandler?.Invoke(m) ?? DefaultManaRate;
public static char[] DefaultGhostChars = { 'o', 'O' };
public static readonly char[] DefaultGhostChars = ['o', 'O'];
public Prompt BeginPrompt(PromptCallback callback, PromptCallback cancelCallback) =>
Prompt = new SimplePrompt(callback, cancelCallback);

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Firewall.cs *
* *
@ -14,28 +14,44 @@
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace Server.Network;
public static class Firewall
{
[ThreadStatic]
private static InternalValidationEntry _validationEntry;
private static readonly Dictionary<IPAddress, bool> _isBlockedCache = new();
private static readonly ConcurrentDictionary<IPAddress, int> _isBlockedCache = [];
private static readonly ReaderWriterLockSlim _firewallLock = new(LockRecursionPolicy.NoRecursion);
private static readonly SortedSet<IFirewallEntry> _firewallSet = new();
private static int _firewallVersion;
private static readonly SortedSet<IFirewallEntry> _firewallSet = [];
public static SortedSet<IFirewallEntry> FirewallSet => _firewallSet;
public static int FirewallSetCount => _firewallSet.Count;
public static void ReadFirewallSet(Action<IReadOnlySet<IFirewallEntry>> callback)
{
_firewallLock.EnterReadLock();
try
{
callback(_firewallSet);
}
finally
{
_firewallLock.ExitReadLock();
}
}
internal static bool IsBlocked(IPAddress address)
{
ref var isBlocked = ref CollectionsMarshal.GetValueRefOrAddDefault(_isBlockedCache, address, out var exists);
if (exists)
if (_isBlockedCache.TryGetValue(address, out var blockVersion) && blockVersion == _firewallVersion)
{
return isBlocked;
return true;
}
if (_validationEntry == null)
@ -47,34 +63,68 @@ public static class Firewall
_validationEntry.Address = address;
}
// Get all entries that are lower than our validation entry
var view = _firewallSet.GetViewBetween(_firewallSet.Min, _validationEntry);
// Loop backward since there shouldn't be any entries where the Min address is higher than ours
foreach (var firewallEntry in view.Reverse())
if (CheckBlocked(_validationEntry))
{
if (firewallEntry.IsBlocked(_validationEntry.MinIpAddress))
{
isBlocked = true;
return true;
}
_isBlockedCache[address] = _firewallVersion;
return true;
}
isBlocked = view.Max?.IsBlocked(_validationEntry.MinIpAddress) == true;
return false;
}
return isBlocked;
private static bool CheckBlocked(IFirewallEntry validationEntry)
{
if (_firewallSet.Count == 0)
{
return false;
}
_firewallLock.EnterReadLock();
try
{
var min = _firewallSet.Min;
if (validationEntry.CompareTo(min) < 0)
{
return false;
}
// Get all entries that are lower than our validation entry
var view = _firewallSet.GetViewBetween(min, validationEntry);
// Loop backward since there shouldn't be any entries where the Min address is higher than ours
foreach (var firewallEntry in view.Reverse())
{
if (firewallEntry.IsBlocked(validationEntry.MinIpAddress))
{
return true;
}
}
return view.Max?.IsBlocked(validationEntry.MinIpAddress) == true;
}
finally
{
_firewallLock.ExitReadLock();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Add(IFirewallEntry firewallEntry)
{
if (_firewallSet.Add(firewallEntry))
_firewallLock.EnterWriteLock();
try
{
_isBlockedCache.Clear();
return true;
if (_firewallSet.Add(firewallEntry))
{
Interlocked.Increment(ref _firewallVersion); // Update version
return true;
}
return false;
}
finally
{
_firewallLock.ExitWriteLock();
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -85,13 +135,20 @@ public static class Firewall
return false;
}
if (_firewallSet.Remove(entry))
_firewallLock.EnterWriteLock();
try
{
_isBlockedCache.Clear();
return true;
if (_firewallSet.Remove(entry))
{
Interlocked.Increment(ref _firewallVersion); // Update version
return true;
}
return false;
}
finally
{
_firewallLock.ExitWriteLock();
}
return false;
}
private class InternalValidationEntry : BaseFirewallEntry

View file

@ -40,12 +40,12 @@ public interface IFirewallEntry : IComparable<IFirewallEntry>
return 1;
}
if (MaxIpAddress < other.MaxIpAddress)
if (MaxIpAddress > other.MaxIpAddress)
{
return -1;
}
if (MaxIpAddress > other.MaxIpAddress)
if (MaxIpAddress < other.MaxIpAddress)
{
return 1;
}

View file

@ -1,108 +0,0 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: IPLimiter.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Generic;
using System.Net;
namespace Server.Misc;
public static class IPLimiter
{
private static readonly SortedSet<IPAccessLog> _connectionAttempts = [];
private static readonly SortedSet<IPAccessLog> _throttledAddresses = [];
private static readonly IPAddress _localHost = IPAddress.Parse("127.0.0.1");
public static TimeSpan ConnectionAttemptsDuration { get; private set; }
public static TimeSpan ConnectionThrottleDuration { get; private set; }
public static bool Enabled { get; private set; }
public static int MaxConnections { get; private set; }
public static void Configure()
{
Enabled = ServerConfiguration.GetOrUpdateSetting("ipLimiter.enable", true);
MaxConnections = ServerConfiguration.GetOrUpdateSetting("ipLimiter.maxConnectionsPerIP", 5);
ConnectionAttemptsDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearConnectionAttemptsDuration", TimeSpan.FromSeconds(10));
ConnectionThrottleDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.connectionThrottleDuration", TimeSpan.FromMinutes(5));
}
private static readonly IPAccessLog _accessCheck = new(IPAddress.None, DateTime.MinValue);
public static bool Verify(IPAddress ourAddress)
{
if (!Enabled || ourAddress.Equals(_localHost))
{
return true;
}
var now = Core.Now;
IPAccessLog accessLog;
while (_throttledAddresses.Count > 0)
{
accessLog = _throttledAddresses.Min;
if (now <= accessLog.Expiration)
{
break;
}
_throttledAddresses.Remove(accessLog);
}
_accessCheck.IPAddress = ourAddress;
if (_connectionAttempts.TryGetValue(_accessCheck, out accessLog))
{
_connectionAttempts.Remove(accessLog);
accessLog.Count++;
accessLog.Expiration = now + ConnectionAttemptsDuration;
if (now <= accessLog.Expiration && accessLog.Count >= MaxConnections)
{
_throttledAddresses.Add(accessLog);
return false;
}
}
else
{
accessLog = new IPAccessLog(ourAddress, now + ConnectionAttemptsDuration);
}
// Add it back so it is sorted properly
_connectionAttempts.Add(accessLog);
return true;
}
private class IPAccessLog : IComparable<IPAccessLog>
{
public IPAddress IPAddress;
public DateTime Expiration;
public int Count;
public IPAccessLog(IPAddress ipAddress, DateTime expiration)
{
IPAddress = ipAddress;
Expiration = expiration;
Count = 1;
}
public int CompareTo(IPAccessLog other) =>
IPAddress.Equals(other.IPAddress) ? 0 : Expiration.CompareTo(other.Expiration);
}
}

View file

@ -0,0 +1,197 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: IPRateLimiter.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace Server.Network;
public class IPRateLimiter
{
private static readonly ConcurrentQueue<IPStats> _statsPool = [];
private const int MaxPoolSize = 32_768;
private readonly SemaphoreSlim _cleanupSignal = new(0, 1);
private readonly ConcurrentDictionary<IPAddress, IPStats> _ipAttempts;
private readonly ConcurrentQueue<IPAddress> _cleanupQueue;
private readonly CancellationTokenSource _cts;
private readonly int _maxAttempts;
private readonly long _timeWindow; // milliseconds
private readonly long _initialBackoff; // milliseconds
private readonly double _backoffMultiplier;
private readonly long _maxBackoff; // milliseconds
public IPRateLimiter(
int maxAttempts, long timeWindow, long initialBackoff, double backoffMultiplier, long maxBackoff,
CancellationToken token
)
{
_ipAttempts = [];
_cleanupQueue = [];
_maxAttempts = maxAttempts;
_timeWindow = timeWindow;
_initialBackoff = initialBackoff;
_backoffMultiplier = backoffMultiplier;
_maxBackoff = maxBackoff;
_cts = CancellationTokenSource.CreateLinkedTokenSource(token);
Task.Run(CleanupLoop, Core.ClosingTokenSource.Token);
}
public bool Verify(IPAddress ip, out int totalAttempts)
{
var nowTicks = Core.TickCount;
var ipStats = _ipAttempts.GetOrAdd(ip, _ => GetOrCreateIPStats());
var added = ipStats.AttemptCount == 0;
lock (ipStats)
{
if (nowTicks - ipStats.LastAttemptTicks > _timeWindow)
{
ipStats.AttemptCount = 1; // Reset
}
else
{
ipStats.AttemptCount++;
}
totalAttempts = ipStats.AttemptCount;
ipStats.LastAttemptTicks = nowTicks;
if (ipStats.BlockUntilTicks - nowTicks > 0)
{
return false;
}
if (ipStats.AttemptCount > _maxAttempts)
{
var backoffTime = Math.Min(
(long)(_initialBackoff * Math.Pow(_backoffMultiplier, ipStats.AttemptCount - _maxAttempts)),
_maxBackoff
);
ipStats.BlockUntilTicks = nowTicks + backoffTime;
return false;
}
if (added)
{
_cleanupQueue.Enqueue(ip);
RunCleanup();
}
}
return true;
}
private static IPStats GetOrCreateIPStats() => _statsPool.TryDequeue(out var stats) ? stats : new IPStats();
private static void ReturnToPool(IPStats stats)
{
stats.Reset();
if (_statsPool.Count < MaxPoolSize)
{
_statsPool.Enqueue(stats);
}
}
private void RunCleanup()
{
if (_cleanupSignal.CurrentCount > 0)
{
return;
}
try
{
_cleanupSignal.Release();
Task.Run(CleanupLoop, _cts.Token);
}
catch
{
// Do nothing
}
}
private async ValueTask CleanupLoop()
{
while (!_cts.IsCancellationRequested)
{
await _cleanupSignal.WaitAsync(_cts.Token);
int maxToProcess = Math.Min(_cleanupQueue.Count, 500);
var nowTicks = Core.TickCount;
for (int i = 0; i < maxToProcess; i++)
{
if (!_cts.IsCancellationRequested)
{
break;
}
if (!_cleanupQueue.TryDequeue(out var ip) || !_ipAttempts.TryGetValue(ip, out var ipStats))
{
continue;
}
lock (ipStats)
{
if (nowTicks - ipStats.LastAttemptTicks < _timeWindow)
{
_cleanupQueue.Enqueue(ip);
continue;
}
if (_ipAttempts.TryRemove(ip, out _))
{
ReturnToPool(ipStats);
}
}
}
if (!_cleanupQueue.IsEmpty)
{
await Task.Delay(TimeSpan.FromMinutes(1), _cts.Token);
try
{
_cleanupSignal.Release();
}
catch
{
// Do nothing
}
}
}
}
private class IPStats
{
public int AttemptCount;
public long LastAttemptTicks;
public long BlockUntilTicks;
public void Reset()
{
AttemptCount = 0;
LastAttemptTicks = 0;
BlockUntilTicks = 0;
}
}
}

View file

@ -33,7 +33,7 @@ public static class OutgoingContainerPackets
return;
}
if (ns.NewSpellbook)
if (Core.AOS && ns.NewSpellbook)
{
ns.SendNewSpellbookContent(book, graphic, offset, content);
}

View file

@ -13,15 +13,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Server.Logging;
using Server.Misc;
namespace Server.Network;
@ -35,8 +37,11 @@ public static class TcpServer
public static IPEndPoint[] ListeningAddresses { get; private set; }
public static Socket[] Listeners { get; private set; }
private static IPRateLimiter _ipRateLimiter;
public static void Start()
{
_ipRateLimiter = new IPRateLimiter(10, 10000, 1000, 2.0, 3_600_000, Core.ClosingTokenSource.Token);
HashSet<IPEndPoint> listeningAddresses = [];
List<Socket> listeners = [];
foreach (var ipep in ServerConfiguration.Listeners)
@ -123,49 +128,101 @@ public static class TcpServer
return null;
}
private static async void BeginAcceptingSockets(Socket listener)
private static async ValueTask BeginAcceptingSockets(Socket listener)
{
while (!Core.Closing)
{
Socket socket = null;
try
{
socket = await listener.AcceptAsync();
var socket = await listener.AcceptAsync(Core.ClosingTokenSource.Token);
var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address;
if (!IPLimiter.Verify(remoteIP))
if (!_ipRateLimiter.Verify(remoteIP, out var totalAttempts))
{
TraceDisconnect("Past IP limit threshold", remoteIP);
logger.Debug("{Address} Past IP limit threshold", remoteIP);
logger.Debug("{Address} Past IP limit threshold ({TotalAttempts})", remoteIP, totalAttempts);
}
else if (Firewall.IsBlocked(remoteIP))
{
TraceDisconnect("Firewalled", remoteIP);
logger.Debug("{Address} Firewalled", remoteIP);
}
else
{
var args = new SocketConnectEventArgs(socket);
EventSink.InvokeSocketConnect(args);
if (args.AllowConnection)
{
_ = new NetState(socket);
continue;
}
TraceDisconnect("Rejected by socket event handler", remoteIP);
// Reject the connection
socket.Send(_socketRejected, SocketFlags.None);
_ = Task.Run(() => ProcessSocketConnection(socket), Core.ClosingTokenSource.Token);
}
CloseSocket(socket);
}
catch
{
// ignored
}
}
}
[ThreadStatic]
private static byte[] _firstBytes;
private static async ValueTask ProcessSocketConnection(Socket socket)
{
_firstBytes ??= GC.AllocateUninitializedArray<byte>(128);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(Core.ClosingTokenSource.Token);
cts.CancelAfter(TimeSpan.FromMilliseconds(500));
try
{
var bytesRead = await socket.ReceiveAsync(_firstBytes, SocketFlags.Peek, cts.Token);
var isValid =
// Sometimes when newer clients are connecting to the game server the first 4 bytes are sent separately
bytesRead == 4 ||
// Support Freeshard Protocol (UOGateway)
bytesRead == 8 &&
BinaryPrimitives.ReadUInt32BigEndian(_firstBytes.AsSpan(4)) is 0xF10004FF or 0xF10004FE ||
// Older clients only send the 4 byte seed first then 0x80
(UOClient.MinRequired == null || UOClient.MinRequired < ClientVersion.Version6050) &&
bytesRead >= 66 && _firstBytes[4] == 0x80 ||
// Newer clients
(UOClient.MaxRequired == null || UOClient.MaxRequired >= ClientVersion.Version6050) && (
// Account Login - 0xEF + 0x80 (83 bytes)
bytesRead >= 83 && _firstBytes[0] == 0xEF && _firstBytes[21] == 0x80 ||
bytesRead == 21 && _firstBytes[0] == 0xEF ||
// Game Login - 4 bytes + 0x91 (69 bytes)
bytesRead >= 69 && _firstBytes[4] == 0x91
);
// TODO: Validate client version is v4 -> v7 for 0xEF packet
// TODO: Validate Account Login seed matches Game Login seed
// TODO: Validate AuthId for 0x91 packet
// TODO: Validate username is ascii and not empty
// TODO: Validate password is ascii and not empty
if (isValid)
{
var args = new SocketConnectEventArgs(socket);
EventSink.InvokeSocketConnect(args);
if (args.AllowConnection)
{
Core.LoopContext.Post(() => _ = new NetState(socket), EventLoopContext.Priority.High);
return;
}
logger.Debug("{Address} Rejected by socket handler", ((IPEndPoint)socket.RemoteEndPoint)!.Address);
cts.TryReset();
cts.CancelAfter(TimeSpan.FromMilliseconds(500));
await socket.SendAsync(_socketRejected, SocketFlags.None, cts.Token);
CloseSocket(socket);
}
else
{
ForceCloseSocket(socket);
}
}
catch
{
ForceCloseSocket(socket);
}
}
@ -182,22 +239,16 @@ public static class TcpServer
}
}
private static void TraceDisconnect(string reason, IPAddress ip)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ForceCloseSocket(Socket socket)
{
try
{
using StreamWriter op = new StreamWriter("network-socket-disconnects.log", true);
op.WriteLine($"# {Core.Now}");
op.WriteLine($"Address: {ip}");
op.WriteLine(reason);
op.WriteLine();
op.WriteLine();
socket.Disconnect(false);
}
catch
finally
{
// ignored
socket.Close(0);
}
}
}

View file

@ -127,7 +127,7 @@ public class Region : IComparable<Region>, IValueLinkListNode<Region>
public const int MinZ = sbyte.MinValue;
public const int MaxZ = sbyte.MaxValue + 1;
public Region(string name, Map map, int priority, params Rectangle2D[] area) : this(
public Region(string name, Map map, int priority, params ReadOnlySpan<Rectangle2D> area) : this(
name,
map,
priority,
@ -147,7 +147,7 @@ public class Region : IComparable<Region>, IValueLinkListNode<Region>
public Region(string name, Map map, Region parent, int priority, params Rectangle3D[] area) : this(name, map, parent, area) =>
Priority = priority;
public Region(string name, Map map, Region parent, params Rectangle2D[] area) : this(
public Region(string name, Map map, Region parent, params ReadOnlySpan<Rectangle2D> area) : this(
name,
map,
parent,
@ -314,7 +314,7 @@ public class Region : IComparable<Region>, IValueLinkListNode<Region>
public static Rectangle3D ConvertTo3D(Rectangle2D rect) =>
new(new Point3D(rect.Start, MinZ), new Point3D(rect.End, MaxZ));
public static Rectangle3D[] ConvertTo3D(Rectangle2D[] rects)
public static Rectangle3D[] ConvertTo3D(ReadOnlySpan<Rectangle2D> rects)
{
var ret = new Rectangle3D[rects.Length];

View file

@ -167,7 +167,7 @@ public sealed unsafe class BinaryFileReader : IDisposable, IGenericReader
public Serial ReadSerial() => _reader.ReadSerial();
/// <summary>
/// Reads the next Byte which helps determin how to read the following Type.
/// Reads the next Byte which helps determine how to read the following Type.
/// <br>If the byte returns 1 => <see cref="ReadStringRaw"/> and translate into a Type via the <see cref="AssemblyHandler"/></br>
/// <br>If the byte returns 2 => <see cref="UnmanagedDataReader.ReadTypeByHash"/></br>
/// <br>else return null</br>

View file

@ -176,12 +176,12 @@ public class GenericEntityPersistence<T> : GenericPersistence, IGenericEntityPer
/**
* Legacy ReadTypes for backward compatibility with old saves that still have a tdb file
*/
private unsafe Dictionary<int, ConstructorInfo> ReadTypes(string savePath)
private unsafe Dictionary<ulong, ConstructorInfo> ReadTypes(string savePath)
{
string typesPath = Path.Combine(savePath, Name, $"{Name}.tdb");
if (!File.Exists(typesPath))
{
return null;
return [];
}
Type[] ctorArguments = [typeof(Serial)];
@ -194,13 +194,13 @@ public class GenericEntityPersistence<T> : GenericPersistence, IGenericEntityPer
var dataReader = new UnmanagedDataReader(ptr, accessor.Length);
var count = dataReader.ReadInt();
var types = new Dictionary<int, ConstructorInfo>(count);
var types = new Dictionary<ulong, ConstructorInfo>(count);
for (var i = 0; i < count; ++i)
{
// Legacy didn't have the null flag check
var typeName = dataReader.ReadStringRaw();
types.Add(i, GetConstructorFor(typeName, AssemblyHandler.FindTypeByName(typeName), ctorArguments));
types.Add((ulong)i, GetConstructorFor(typeName, AssemblyHandler.FindTypeByName(typeName), ctorArguments));
}
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
@ -258,14 +258,9 @@ public class GenericEntityPersistence<T> : GenericPersistence, IGenericEntityPer
var version = dataReader.ReadInt();
Dictionary<int, ConstructorInfo> ctors = [];
var ctors = version < 2 ? ReadTypes(Path.GetDirectoryName(filePath)) : [];
if (version < 2)
{
ctors = ReadTypes(Path.GetDirectoryName(filePath));
}
if (typesDb == null && ctors == null)
if (typesDb == null && ctors.Count == 0)
{
return;
}
@ -278,7 +273,7 @@ public class GenericEntityPersistence<T> : GenericPersistence, IGenericEntityPer
for (var i = 0; i < count; ++i)
{
ConstructorInfo ctor;
ulong hash;
// Version 2 & 3 with SerializedTypes.db
if (version >= 2)
{
@ -288,13 +283,16 @@ public class GenericEntityPersistence<T> : GenericPersistence, IGenericEntityPer
throw new Exception($"Invalid type flag, expected 2 but received {flag}.");
}
var hash = dataReader.ReadULong();
typesDb!.TryGetValue(hash, out var typeName);
ctor = GetConstructorFor(typeName, AssemblyHandler.FindTypeByHash(hash), ctorArguments);
hash = dataReader.ReadULong();
}
else
{
ctor = ctors?[dataReader.ReadInt()];
hash = (ulong)dataReader.ReadInt(); // Legacy RunUO tdb index
}
if (!ctors.TryGetValue(hash, out var ctor) && typesDb?.TryGetValue(hash, out var typeName) == true)
{
ctors[hash] = ctor = GetConstructorFor(typeName, AssemblyHandler.FindTypeByHash(hash), ctorArguments);
}
Serial serial = (Serial)dataReader.ReadUInt();

View file

@ -14,6 +14,7 @@
*************************************************************************/
using System;
using System.Buffers;
using System.Collections;
using System.IO;
using System.Net;
@ -51,7 +52,7 @@ public interface IGenericReader
var delta => new DateTime(delta + DateTime.UtcNow.Ticks, DateTimeKind.Utc)
};
}
decimal ReadDecimal() => new(stackalloc int[4] { ReadInt(), ReadInt(), ReadInt(), ReadInt() });
decimal ReadDecimal() => new([ReadInt(), ReadInt(), ReadInt(), ReadInt()]);
int ReadEncodedInt()
{
int v = 0, shift = 0;
@ -119,14 +120,19 @@ public interface IGenericReader
public BitArray ReadBitArray()
{
var byteArrayLength = ReadEncodedInt();
int bitLength = ReadEncodedInt();
int byteLength = (bitLength + 7) / 8;
// We need an exact array size since the ctor doesn't allow for offset/length, not much we can do at this point.
var byteArray = new byte[byteArrayLength];
Read(byteArray);
return new BitArray(byteArray);
var buffer = ArrayPool<byte>.Shared.Rent(byteLength);
try
{
Read(buffer.AsSpan(0, byteLength));
return new BitArray(buffer) { Length = bitLength };
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
TextDefinition ReadTextDefinition()

View file

@ -163,14 +163,21 @@ public interface IGenericWriter
public void Write(BitArray bitArray)
{
var bytesLength = (bitArray.Length - 1 + (1 << 3)) >>> 3;
var arrayBuffer = ArrayPool<byte>.Shared.Rent(bytesLength);
int bitLength = bitArray.Length;
int byteLength = (bitLength + 7) / 8;
WriteEncodedInt(bytesLength);
bitArray.CopyTo(arrayBuffer, 0);
WriteEncodedInt(bitLength);
Write(arrayBuffer.AsSpan(0, bytesLength));
ArrayPool<byte>.Shared.Return(arrayBuffer);
var arrayBuffer = ArrayPool<byte>.Shared.Rent(byteLength);
try
{
bitArray.CopyTo(arrayBuffer, 0);
Write(arrayBuffer.AsSpan(0, byteLength));
}
finally
{
ArrayPool<byte>.Shared.Return(arrayBuffer);
}
}
void Write(TextDefinition def)

View file

@ -218,7 +218,7 @@ public unsafe class UnmanagedDataReader : IGenericReader
public Serial ReadSerial() => (Serial)ReadUInt();
/// <summary>
/// Reads the next Byte which helps determin how to read the following Type.
/// Reads the next Byte which helps determine how to read the following Type.
/// <br>If the byte returns 1 => <see cref="ReadStringRaw"/> and translate into a Type via the <see cref="AssemblyHandler"/></br>
/// <br>If the byte returns 2 => <see cref="ReadTypeByHash"/></br>
/// <br>else return null</br>

View file

@ -37,12 +37,18 @@
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
<PackageReference Include="LibDeflate.Bindings" Version="1.0.2.120" />
<PackageReference Include="PollGroup" Version="1.6.1" />
<PackageReference Include="System.IO.Hashing" Version="9.0.1" />
<PackageReference Include="System.IO.Hashing" Version="9.0.4" />
<PackageReference Include="ModernUO.Serialization.Annotations" Version="2.9.1" />
<PackageReference Include="ModernUO.Serialization.Generator" Version="2.12.18" />
<PackageReference Include="ModernUO.Serialization.Generator" Version="2.12.20" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Migrations/*.v*.json" />
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Server.Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>UOContent.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: TextEncoding.cs *
* *
@ -16,6 +16,7 @@
using System;
using System.Runtime.CompilerServices;
using System.Text;
using Server.Buffers;
namespace Server.Text;
@ -114,33 +115,63 @@ public static class TextEncoding
_ => 1
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsSafeChar(ushort c) => c is >= 0x20 and < 0xFFFE;
public static string GetString(ReadOnlySpan<byte> span, Encoding encoding, bool safeString = false)
{
string s = encoding.GetString(span);
if (!safeString)
{
return s;
return encoding.GetString(span);
}
ReadOnlySpan<char> chars = s.AsSpan();
var charCount = encoding.GetMaxCharCount(span.Length);
using var sb = new ValueStringBuilder(stackalloc char[256]);
var hasDoneAnyReplacements = false;
char[] rentedChars = null;
Span<char> chars = charCount <= 256
? stackalloc char[charCount]
: rentedChars = STArrayPool<char>.Shared.Rent(charCount);
for (int i = 0, last = 0; i < chars.Length; i++)
try
{
if (!IsSafeChar(chars[i]) && i == chars.Length - 1)
var length = encoding.GetChars(span, chars);
chars = chars[..length];
var index = chars.IndexOfAnyExceptInRange((char)0x20, (char)0xFFFD);
if (index == -1)
{
hasDoneAnyReplacements = true;
sb.Append(chars.Slice(last, i - last));
last = i + 1; // Skip the unsafe char
return new string(chars);
}
using var sb = charCount <= 256
? new ValueStringBuilder(stackalloc char[charCount])
: ValueStringBuilder.Create(charCount);
while (index != -1)
{
sb.Append(chars[..index]);
if (index + 1 < chars.Length)
{
chars = chars[(index + 1)..];
index = chars.IndexOfAnyExceptInRange((char)0x20, (char)0xFFFD);
}
else
{
index = -1;
}
}
if (chars.Length > 0)
{
sb.Append(chars);
}
return sb.ToString();
}
finally
{
if (rentedChars != null)
{
STArrayPool<char>.Shared.Return(rentedChars);
}
}
return !hasDoneAnyReplacements ? s : sb.ToString();
}
}

View file

@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Html.cs *
* *
@ -23,65 +23,219 @@ namespace Server;
public static class Html
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(this string text, int color) => $"<BASEFONT COLOR=#{color:X6}>{text}</BASEFONT>";
public static RawInterpolatedStringHandler Color(
scoped ref RawInterpolatedStringHandler textHandler,
ReadOnlySpan<char> color,
int size = -1, byte fontStyle = 0
)
{
var handler = textHandler.Text.Color(color, size, fontStyle);
textHandler.Clear();
return handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Color(this ReadOnlySpan<char> text, int color) =>
$"<BASEFONT COLOR=#{color:X6}>{text}</BASEFONT>";
public static RawInterpolatedStringHandler Color(
this ReadOnlySpan<char> text,
ReadOnlySpan<char> color,
int size = -1,
byte fontStyle = 0
)
{
if (color != Span<char>.Empty)
{
if (size > -1)
{
if (fontStyle > 0)
{
return $"<BASEFONT COLOR={color} SIZE={size} STYLE={fontStyle}>{text}</BASEFONT>";
}
return $"<BASEFONT COLOR={color} SIZE={size}>{text}</BASEFONT>";
}
if (fontStyle > 0)
{
return $"<BASEFONT COLOR={color} STYLE={fontStyle}>{text}</BASEFONT>";
}
return $"<BASEFONT COLOR={color}>{text}</BASEFONT>";
}
if (size > -1)
{
if (fontStyle > 0)
{
return $"<BASEFONT SIZE={size} STYLE={fontStyle}>{text}</BASEFONT>";
}
return $"<BASEFONT SIZE={size}>{text}</BASEFONT>";
}
if (fontStyle > 0)
{
return $"<BASEFONT STYLE={fontStyle}>{text}</BASEFONT>";
}
return $"{text}";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(this string text, int color, int size) => $"<BASEFONT COLOR=#{color:X6} SIZE={size}>{text}</BASEFONT>";
public static RawInterpolatedStringHandler Color(
this ReadOnlySpan<char> text,
int color,
int size = -1,
byte fontStyle = 0
)
{
if (color > -1)
{
return text.Color($"#{color:X6}", size, fontStyle);
}
return text.Color((ReadOnlySpan<char>)default, size, fontStyle);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(this string text, string color) => $"<BASEFONT COLOR={color}>{text}</BASEFONT>";
public static RawInterpolatedStringHandler Color(
scoped ref RawInterpolatedStringHandler textHandler,
int color,
int size = -1,
byte fontStyle = 0
)
{
var handler = textHandler.Text.Color(color, size, fontStyle);
textHandler.Clear();
return handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Color(this string text, string color, int size) =>
$"<BASEFONT COLOR={color} SIZE={size}>{text}</BASEFONT>";
public static string Color(
this string text,
ReadOnlySpan<char> color,
int size = -1,
byte fontStyle = 0
)
{
var textHandler = ((ReadOnlySpan<char>)text).Color(color, size, fontStyle);
var str = textHandler.Text.ToString();
textHandler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(this string text) => $"<CENTER>{text}</CENTER>";
public static string Color(
this string text,
int color,
int size = -1,
byte fontStyle = 0
)
{
var textHandler = ((ReadOnlySpan<char>)text).Color(color, size, fontStyle);
var str = textHandler.Text.ToString();
textHandler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Center(this ReadOnlySpan<char> text) => $"<CENTER>{text}</CENTER>";
public static string Center(
this string text, int color, int size = -1, byte fontStyle = 0
)
{
var handler = Center((ReadOnlySpan<char>)text, color, size, fontStyle);
var str = handler.Text.ToString();
handler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(this string text, int color) =>
$"<BASEFONT COLOR=#{color:X6}><CENTER>{text}</CENTER></BASEFONT>";
public static string Center(
this string text, ReadOnlySpan<char> color, int size = -1, byte fontStyle = 0
)
{
var handler = Center((ReadOnlySpan<char>)text, color, size, fontStyle);
var str = handler.Text.ToString();
handler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Center(this ReadOnlySpan<char> text, int color) =>
$"<BASEFONT COLOR=#{color:X6}><CENTER>{text}</CENTER></BASEFONT>";
public static string Center(this string text) => text.Center(-1);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(this string text, int color, int size) =>
$"<BASEFONT COLOR=#{color:X6} SIZE={size}><CENTER>{text}</CENTER></BASEFONT>";
public static RawInterpolatedStringHandler Center(this ReadOnlySpan<char> text) => Center(text, -1);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(this string text, string color) =>
$"<BASEFONT COLOR={color}><CENTER>{text}</CENTER></BASEFONT>";
public static RawInterpolatedStringHandler Center(
this ReadOnlySpan<char> text, int color, int size = -1, byte fontStyle = 0
) => Color($"<CENTER>{text}</CENTER>", color, size, fontStyle);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Center(this string text, string color, int size) =>
$"<BASEFONT COLOR={color} SIZE={size}><CENTER>{text}</CENTER></BASEFONT>";
public static RawInterpolatedStringHandler Center(
this ReadOnlySpan<char> text, ReadOnlySpan<char> color, int size = -1, byte fontStyle = 0
) => Color($"<CENTER>{text}</CENTER>", color, size, fontStyle);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this string text) => $"<RIGHT>{text}</RIGHT>";
public static RawInterpolatedStringHandler Center(
scoped ref RawInterpolatedStringHandler textHandler, int color = -1, int size = -1, byte fontStyle = 0
)
{
var handler = textHandler.Text.Center(color, size, fontStyle);
textHandler.Clear();
return handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this string text, int color) =>
$"<BASEFONT COLOR=#{color:X6}><RIGHT>{text}</RIGHT></BASEFONT>";
public static string Right(
this string text, int color, int size = -1, byte fontStyle = 0
)
{
var handler = Right((ReadOnlySpan<char>)text, color, size, fontStyle);
var str = handler.Text.ToString();
handler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this string text, int color, int size) =>
$"<BASEFONT COLOR=#{color:X6} SIZE={size}><RIGHT>{text}</RIGHT></BASEFONT>";
public static string Right(
this string text, ReadOnlySpan<char> color, int size = -1, byte fontStyle = 0
)
{
var handler = Right((ReadOnlySpan<char>)text, color, size, fontStyle);
var str = handler.Text.ToString();
handler.Clear();
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this string text, string color) => $"<BASEFONT COLOR={color}><RIGHT>{text}</RIGHT></BASEFONT>";
public static string Right(this string text) => text.Right(-1);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Right(this string text, string color, int size) =>
$"<BASEFONT COLOR={color} SIZE={size}><RIGHT>{text}</RIGHT></BASEFONT>";
public static RawInterpolatedStringHandler Right(
scoped ref RawInterpolatedStringHandler textHandler, int color = -1, int size = -1, byte fontStyle = 0
)
{
var handler = textHandler.Text.Right(color, size, fontStyle);
textHandler.Clear();
return handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Right(
this ReadOnlySpan<char> text, int color, int size = -1, byte fontStyle = 0
) => Color($"<RIGHT>{text}</RIGHT>", color, size, fontStyle);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Right(
this ReadOnlySpan<char> text, ReadOnlySpan<char> color, int size = -1, byte fontStyle = 0
) => Color($"<RIGHT>{text}</RIGHT>", color, size, fontStyle);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Right(this ReadOnlySpan<char> text) => text.Right(-1);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string EscapeHtml(this string input) =>

View file

@ -745,7 +745,7 @@ public static partial class Utility
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T RandomList<T>(params T[] list) => list.RandomElement();
public static T RandomList<T>(params ReadOnlySpan<T> list) => list.RandomElement();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T RandomElement<T>(this ReadOnlySpan<T> list) => list.Length == 0 ? default : list[Random(list.Length)];
@ -1305,15 +1305,15 @@ public static partial class Utility
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T[] Combine<T>(this IList<T> source, params IList<T>[] arrays) =>
public static T[] Combine<T>(this IList<T> source, params ReadOnlySpan<IList<T>> arrays) =>
source.Combine(false, arrays);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T[] CombinePooled<T>(this IList<T> source, params IList<T>[] arrays) =>
public static T[] CombinePooled<T>(this IList<T> source, params ReadOnlySpan<IList<T>> arrays) =>
source.Combine(true, arrays);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T[] Combine<T>(this IList<T> source, bool pooled, params IList<T>[] arrays)
public static T[] Combine<T>(this IList<T> source, bool pooled, params ReadOnlySpan<IList<T>> arrays)
{
var totalLength = source.Count;
foreach (var arr in arrays)
@ -1468,4 +1468,15 @@ public static partial class Utility
table.Remove(key);
table.Add(key, value);
}
public static DateTime LocalToUtc(this DateTime local, TimeZoneInfo tz)
{
if (tz.IsAmbiguousTime(local))
{
var offsets = tz.GetAmbiguousTimeOffsets(local);
return DateTime.SpecifyKind(local - offsets[1], DateTimeKind.Utc);
}
return DateTime.SpecifyKind(local - tz.GetUtcOffset(local), DateTimeKind.Utc);
}
}

View file

@ -492,7 +492,12 @@ public static class World
else
{
logger.Warning($"Attempted to call World.RemoveEntity with '{entity.GetType()}'. Must be a mobile or item.");
return;
}
#if TRACK_LEAKS
EntityFinalizationTracker.TrackEntity(entity);
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View file

@ -1,13 +1,14 @@
using System;
using System.Reflection;
using Server.Misc;
using Xunit;
namespace Server.Tests;
internal class ServerFixture : IDisposable
[CollectionDefinition("Sequential UOContent Tests", DisableParallelization = true)]
public class UOContentFixture : ICollectionFixture<UOContentFixture>, IDisposable
{
// Global setup
static ServerFixture()
public UOContentFixture()
{
Core.ApplicationAssembly = Assembly.GetExecutingAssembly();
Core.LoopContext = new EventLoopContext();
@ -40,8 +41,16 @@ internal class ServerFixture : IDisposable
World.ExitSerializationThreads();
}
private static int _counter;
public void Dispose()
{
_counter++;
if (_counter > 1)
{
throw new Exception("NO!");
}
Timer.Init(0);
}
}

View file

@ -0,0 +1,35 @@
using Server.Misc;
using Xunit;
namespace Server.Tests.Accounting;
public class AccountHandlerTests
{
[Theory]
[InlineData("", false)] // Empty username
[InlineData(" ", false)] // Single space
[InlineData(".", false)] // Single period
[InlineData("Invalid<Char", false)] // Contains forbidden character
[InlineData("EndsWithSpace ", false)] // Ends with space
[InlineData("EndsWithPeriod.", false)] // Ends with period
[InlineData(" StartsWithSpace", false)] // Starts with space
[InlineData("ValidUser123", true)] // Standard Username
[InlineData("Valid.User", true)] // Valid with period
[InlineData("ValidUser!@#", true)] // Contains valid special characters
public void IsValidUsername_ValidatesCorrectly(string username, bool expected)
{
Assert.Equal(expected, AccountHandler.IsValidUsername(username));
}
[Theory]
[InlineData("", false)] // Empty password
[InlineData("Invalid\x01Char", false)] // Contains invalid ASCII character
[InlineData("ValidPass123!", true)] // Standard Password
[InlineData(" ", true)] // Single space
[InlineData("ValidPass!@#", true)] // Valid special characters
[InlineData("ValidPassWithLength1234567890", true)] // Long valid password
public void IsValidPassword_ValidatesCorrectly(string password, bool expected)
{
Assert.Equal(expected, AccountHandler.IsValidPassword(password));
}
}

View file

@ -17,30 +17,30 @@ public class PasswordProtectionTest
[InlineData(typeof(HashAlgorithmPasswordProtection), "SHA2")]
public void TestValidates(Type protectionType, string algorithmType)
{
IPasswordProtection passwordProtection;
if (protectionType == typeof(HashAlgorithmPasswordProtection))
IPasswordProtection passwordProtection;
if (protectionType == typeof(HashAlgorithmPasswordProtection))
{
passwordProtection = algorithmType switch
{
passwordProtection = algorithmType switch
{
"SHA1" => HashAlgorithmPasswordProtection.SHA1Instance,
"SHA2" => HashAlgorithmPasswordProtection.SHA2Instance,
_ => HashAlgorithmPasswordProtection.MD5Instance,
};
}
else
{
passwordProtection = Activator.CreateInstance(protectionType) as IPasswordProtection;
}
if (passwordProtection == null)
{
Assert.Fail($"{protectionType.Name} is not an IPasswordProtection.");
}
var encryptedPassword = passwordProtection.EncryptPassword(plainPassword);
Assert.True(passwordProtection.ValidatePassword(encryptedPassword, plainPassword));
"SHA1" => HashAlgorithmPasswordProtection.SHA1Instance,
"SHA2" => HashAlgorithmPasswordProtection.SHA2Instance,
_ => HashAlgorithmPasswordProtection.MD5Instance,
};
}
else
{
passwordProtection = Activator.CreateInstance(protectionType) as IPasswordProtection;
}
if (passwordProtection == null)
{
Assert.Fail($"{protectionType.Name} is not an IPasswordProtection.");
}
var encryptedPassword = passwordProtection.EncryptPassword(plainPassword);
Assert.True(passwordProtection.ValidatePassword(encryptedPassword, plainPassword));
}
[Theory]
[InlineData(typeof(Argon2PasswordProtection), null)]
@ -50,28 +50,28 @@ public class PasswordProtectionTest
[InlineData(typeof(HashAlgorithmPasswordProtection), "SHA2")]
public void TestPasswordDoesNotValidate(Type protectionType, string algorithmType)
{
IPasswordProtection passwordProtection;
if (protectionType == typeof(HashAlgorithmPasswordProtection))
IPasswordProtection passwordProtection;
if (protectionType == typeof(HashAlgorithmPasswordProtection))
{
passwordProtection = algorithmType switch
{
passwordProtection = algorithmType switch
{
"SHA1" => HashAlgorithmPasswordProtection.SHA1Instance,
"SHA2" => HashAlgorithmPasswordProtection.SHA2Instance,
_ => HashAlgorithmPasswordProtection.MD5Instance,
};
}
else
{
passwordProtection = Activator.CreateInstance(protectionType) as IPasswordProtection;
}
if (passwordProtection == null)
{
Assert.Fail($"{protectionType.Name} is not an IPasswordProtection.");
}
var encryptedPassword = passwordProtection.EncryptPassword(plainPassword);
Assert.False(passwordProtection.ValidatePassword(encryptedPassword, "Not the same password"));
"SHA1" => HashAlgorithmPasswordProtection.SHA1Instance,
"SHA2" => HashAlgorithmPasswordProtection.SHA2Instance,
_ => HashAlgorithmPasswordProtection.MD5Instance,
};
}
}
else
{
passwordProtection = Activator.CreateInstance(protectionType) as IPasswordProtection;
}
if (passwordProtection == null)
{
Assert.Fail($"{protectionType.Name} is not an IPasswordProtection.");
}
var encryptedPassword = passwordProtection.EncryptPassword(plainPassword);
Assert.False(passwordProtection.ValidatePassword(encryptedPassword, "Not the same password"));
}
}

View file

@ -12,12 +12,12 @@ public class ChatPacketTests
[InlineData("ENU", 200, "a third param", "another param")]
public void TestSendChatMessage(string lang, int number, string param1, string param2)
{
var expected = new ChatMessagePacket(lang, number, param1, param2).Compile();
var expected = new ChatMessagePacket(lang, number, param1, param2).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendChatMessage(lang, number, param1, param2);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendChatMessage(lang, number, param1, param2);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
}
}

View file

@ -6,23 +6,23 @@ public sealed class ChatMessagePacket : Packet
{
public ChatMessagePacket(string lang, int number, string param1, string param2) : base(0xB2)
{
param1 ??= string.Empty;
param2 ??= string.Empty;
param1 ??= string.Empty;
param2 ??= string.Empty;
EnsureCapacity(13 + (param1.Length + param2.Length) * 2);
EnsureCapacity(13 + (param1.Length + param2.Length) * 2);
Stream.Write((ushort)(number - 20));
Stream.Write((ushort)(number - 20));
if (lang != null)
{
Stream.WriteAsciiFixed(lang, 4);
}
else
{
Stream.Write(0);
}
Stream.WriteBigUniNull(param1);
Stream.WriteBigUniNull(param2);
if (lang != null)
{
Stream.WriteAsciiFixed(lang, 4);
}
}
else
{
Stream.Write(0);
}
Stream.WriteBigUniNull(param1);
Stream.WriteBigUniNull(param2);
}
}

View file

@ -0,0 +1,313 @@
using System;
using System.Collections.Generic;
using Server;
using Server.Engines.Events;
using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class EventSchedulerTests
{
// Test implementations for controlled testing
private class TestRecurrencePattern : IRecurrencePattern
{
private readonly TimeSpan _interval;
private readonly int _maxOccurrences;
private int _currentOccurrence;
public TestRecurrencePattern(TimeSpan interval, int maxOccurrences = int.MaxValue)
{
_interval = interval;
_maxOccurrences = maxOccurrences;
_currentOccurrence = 0;
}
public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone)
{
_currentOccurrence++;
if (_currentOccurrence > _maxOccurrences)
{
return DateTime.MaxValue;
}
return afterUtc + _interval;
}
}
private class TestScheduledEvent : ScheduledEvent
{
public int CallCount { get; private set; }
public Action Callback { get; }
public TestScheduledEvent(TimeOnly time, Action callback, IRecurrencePattern recurrence = null)
: base(time, recurrence)
{
CallCount = 0;
Callback = callback;
}
public override void OnEvent()
{
CallCount++;
Callback?.Invoke();
}
}
private static void Init()
{
Core._now = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
Timer.Init(0);
EventScheduler.Configure();
}
private static void Finish()
{
EventScheduler.Shared.Stop();
}
[Fact]
public void ScheduleEvent_ExecutesCallback_HappyPath()
{
Init();
try
{
bool called = false;
var evt = EventScheduler.Shared.ScheduleEvent(
Core._now,
TimeOnly.FromDateTime(Core._now),
() => called = true
);
Assert.Equal(Core._now, evt.NextOccurrence);
Timer.Slice(8);
Assert.True(called);
Assert.Equal(DateTime.MaxValue, evt.NextOccurrence);
evt.Cancel();
}
finally
{
Finish();
}
}
[Fact]
public void Scheduler_OrdersEventsByNextOccurrence()
{
Init();
try
{
var executionOrder = new List<int>();
// Create events with staggered occurrences
var evt1Start = Core._now.AddSeconds(10);
var evt1 = new TestScheduledEvent(
TimeOnly.FromDateTime(Core._now.AddSeconds(10)),
() => executionOrder.Add(1)
);
var evt2Start = Core._now.AddSeconds(20);
var evt2 = new TestScheduledEvent(
TimeOnly.FromDateTime(evt2Start),
() => executionOrder.Add(2)
);
var evt3Start = Core._now.AddSeconds(30);
var evt3 = new TestScheduledEvent(
TimeOnly.FromDateTime(evt3Start),
() => executionOrder.Add(3)
);
// Add out of order
evt3.Schedule(evt3Start);
evt1.Schedule(evt1Start);
evt2.Schedule(evt2Start);
// Advance time to after all events
Core._now = Core._now.AddSeconds(40);
Timer.Slice(8);
// Verify they executed in time order, not addition order
Assert.Equal([1, 2, 3], executionOrder);
evt1.Cancel();
evt2.Cancel();
evt3.Cancel();
}
finally
{
Finish();
}
}
[Fact]
public void Scheduler_HandlesRecurringEvents()
{
for (var i = 0; i < 100_000; i++)
{
Init();
try
{
int callCount = 0;
// Create a recurrence pattern that fires every 10 seconds, up to 3 times
var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3);
var evt = new TestScheduledEvent(
TimeOnly.FromDateTime(Core._now),
() => callCount++,
recurrence
);
evt.Schedule(Core._now);
// Advance time to after all occurrences should have happened
Core._now = Core._now.AddSeconds(50);
Timer.Slice(8);
// Should have fired 4 times (3 recurrences)
Assert.Equal(3, callCount);
Assert.Equal(3, evt.CallCount);
evt.Cancel();
}
finally
{
Finish();
}
}
}
[Fact]
public void Scheduler_HandlesExceptionsInEvents()
{
Init();
try
{
bool failedEventCalled = false;
bool laterEventCalled = false;
// Event that throws exception
var failedEvt = EventScheduler.Shared.ScheduleEvent(
Core._now.AddSeconds(10),
() =>
{
failedEventCalled = true;
throw new Exception("Test exception");
}
);
// Later event that should still execute
var laterEvt = EventScheduler.Shared.ScheduleEvent(
Core._now.AddSeconds(20),
() => laterEventCalled = true
);
// Advance time to after both events
Core._now = Core._now.AddSeconds(30);
Timer.Slice(8);
// Both should have been called despite the exception
Assert.True(failedEventCalled);
Assert.True(laterEventCalled);
failedEvt.Cancel();
laterEvt.Cancel();
}
finally
{
Finish();
}
}
[Fact]
public void Scheduler_RemovesEventsCorrectly()
{
Init();
try
{
bool eventCalled = false;
var evt = EventScheduler.Shared.ScheduleEvent(
Core._now.AddSeconds(10),
() => eventCalled = true
);
// Remove before execution
evt.Cancel();
// Advance time
Core._now = Core._now.AddSeconds(20);
Timer.Slice(8);
// Event should not have executed
Assert.False(eventCalled);
}
finally
{
Finish();
}
}
[Fact]
public void Scheduler_HandlesEventsWithEndDates()
{
Init();
try
{
int callCount = 0;
// Create a recurrence pattern that fires every 10 seconds
var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10));
// Create a custom event with an end date
var startTime = Core._now;
var endTime = Core._now.AddSeconds(36);
var customEvent = new CustomEvent(
TimeOnly.FromDateTime(startTime),
endTime,
recurrence,
() => callCount++
);
customEvent.Schedule(startTime);
Core._now = Core._now.AddSeconds(50);
Timer.Slice(8);
// Should have fired 3 times only (initial + 2 within the timeframe)
Assert.Equal(3, callCount);
}
finally
{
Finish();
}
}
private class CustomEvent : ScheduledEvent
{
private readonly Action _callback;
public CustomEvent(
TimeOnly time,
DateTime endOn,
IRecurrencePattern recurrence,
Action callback
) : base(time, endOn, recurrence) => _callback = callback;
public override void OnEvent()
{
_callback?.Invoke();
}
}
}

View file

@ -0,0 +1,313 @@
using System;
using Server;
using Server.Engines.Events;
using Xunit;
namespace UOContent.Tests;
public class RecurrencePatternTests
{
[Theory]
[InlineData(2024, 5, 15, 12, 30, 1, 2024, 5, 15, 13, 30)] // Normal case
[InlineData(2024, 5, 15, 23, 30, 2, 2024, 5, 16, 1, 30)] // Cross day boundary
public void HourlyRecurrencePattern_GetNextOccurrence_ReturnsCorrectTime(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalHours,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new HourlyRecurrencePattern(intervalHours);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(0, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 5, 15, 12, 30, 1, 2024, 5, 16, 12, 30)] // Next day
[InlineData(2024, 5, 31, 12, 30, 2, 2024, 6, 2, 12, 30)] // Across month boundary
public void DailyRecurrencePattern_GetNextOccurrence_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new DailyRecurrencePattern(intervalDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 5, 15, 12, 30, 1, AllowedDays.All, 2024, 5, 16, 12, 30)] // Next day (Thursday)
[InlineData(2024, 5, 15, 12, 30, 1, AllowedDays.Monday, 2024, 5, 20, 12, 30)] // Next Monday
[InlineData(2024, 5, 15, 12, 30, 2, AllowedDays.Wednesday, 2024, 5, 29, 12, 30)] // Biweekly Wednesday
public void WeeklyRecurrencePattern_BasicPatterns_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 12, 30, 12, 30, 1, AllowedDays.Monday, 2025, 1, 6, 12, 30)] // Across year boundary
[InlineData(2024, 5, 31, 12, 30, 1, AllowedDays.Saturday, 2024, 6, 1, 12, 30)] // Across month boundary
public void WeeklyRecurrencePattern_CrossBoundaries_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 5, 15, 12, 30, AllowedMonths.January | AllowedMonths.February, AllowedDays.Wednesday, 2025, 1, 1, 12, 30)] // Skip to January
[InlineData(2024, 12, 15, 12, 30, AllowedMonths.January | AllowedMonths.December, AllowedDays.Wednesday, 2024, 12, 18, 12, 30)] // Same month
[InlineData(2024, 1, 31, 12, 30, AllowedMonths.February | AllowedMonths.April, AllowedDays.Saturday, 2024, 2, 3, 12, 30)] // Next month allowed
public void WeeklyRecurrencePattern_MonthFiltering_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
AllowedMonths allowedMonths, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(1, allowedMonths, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData("America/New_York", 2024, 3, 9, 12, 0, 1, AllowedDays.Sunday, 2024, 3, 10, 12, 0)] // Before spring forward
[InlineData("America/New_York", 2024, 3, 10, 2, 30, 1, AllowedDays.Sunday, 2024, 3, 17, 2, 30)] // During spring forward (invalid time)
[InlineData("America/New_York", 2024, 11, 2, 12, 0, 1, AllowedDays.Sunday, 2024, 11, 3, 12, 0)] // Before fall back
[InlineData("America/New_York", 2024, 11, 3, 1, 30, 1, AllowedDays.Sunday, 2024, 11, 10, 1, 30)] // During fall back (ambiguous time)
public void WeeklyRecurrencePattern_DSTTransitions_HandlesCorrectly(
string tzId, int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId);
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
var startLocal = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0);
var afterUtc = startLocal.LocalToUtc(tz);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(result, tz);
Assert.Equal(new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0), resultLocal);
}
[Theory]
[InlineData(2024, 5, 29, 12, 0, AllowedDays.None, 2024, 5, 29, 12, 30)] // Default to current day
[InlineData(2024, 5, 29, 12, 0, AllowedDays.Wednesday | AllowedDays.Friday, 2024, 5, 29, 12, 30)] // Current day is Wednesday
[InlineData(2024, 5, 29, 12, 0, AllowedDays.Monday | AllowedDays.Friday, 2024, 5, 31, 12, 30)] // Next allowed day (Friday)
public void WeeklyRecurrencePattern_DaysOfWeekHandling_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(1, AllowedMonths.All, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 1, 29, 12, 0, 1, 2024, 1, 29, 12, 30, AllowedMonths.January | AllowedMonths.March, 2024, 1, 30, 12, 30)] // Current month allowed
[InlineData(2024, 1, 31, 12, 0, 1, 2024, 1, 31, 12, 30, AllowedMonths.January | AllowedMonths.March, 2024, 3, 1, 12, 30)] // Skip February
public void WeeklyRecurrencePattern_WeekSpanningMonths_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute,
AllowedMonths allowedMonths,
int nextExpectedYear, int nextExpectedMonth, int nextExpectedDay, int nextExpectedHour, int nextExpectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(intervalWeeks, allowedMonths);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act - first call should hit expectedDate
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
// Call again - should move to next available day in allowed months
var nextResult = pattern.GetNextOccurrence(result, time, tz);
var nextExpected = new DateTime(nextExpectedYear, nextExpectedMonth, nextExpectedDay, nextExpectedHour, nextExpectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(nextExpected, nextResult);
}
[Theory]
[InlineData(2024, 11, 3, 1, 30, DayOfWeek.Sunday, OrdinalDayOccurrence.First, "America/New_York", 2024, 11, 3)]
[InlineData(2024, 3, 31, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Last, "America/New_York", 2024, 3, 31)]
[InlineData(2024, 3, 31, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Fifth, "America/New_York", 2024, 3, 31)]
[InlineData(2024, 4, 3, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Fifth, "America/New_York", 2024, 6, 30)] // June has 5 Sundays (2, 9, 16, 23, 30)
public void MonthlyOrdinalRecurrencePattern_GetNextOccurrence_ReturnsCorrectDate(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
DayOfWeek dow, OrdinalDayOccurrence ordinal, string tzId,
int expectedYear, int expectedMonth, int expectedDay)
{
// Arrange
var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId);
var pattern = new MonthlyOrdinalRecurrencePattern(ordinal, dow);
// Use a day BEFORE the expected date
var startLocal = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0);
var beforeUtc = startLocal.AddDays(-1).LocalToUtc(tz);
var timeOnly = new TimeOnly(startHour, startMinute);
// Act
var result = pattern.GetNextOccurrence(beforeUtc, timeOnly, tz);
// Assert - convert back to local for comparison
var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(result, tz);
Assert.Equal(new DateTime(expectedYear, expectedMonth, expectedDay, startHour, startMinute, 0), resultLocal);
}
[Theory]
[InlineData("America/New_York", 2024, 3, 10, 2, 30)] // Spring forward - 2:30 AM doesn't exist
[InlineData("America/New_York", 2024, 11, 3, 1, 30)] // Fall back - 1:30 AM happens twice
public void MonthlyRecurrencePattern_DSTTransitions_HandlesCorrectly(
string tzId, int year, int month, int day, int hour, int minute)
{
// Arrange
var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId);
var pattern = new MonthlyRecurrencePattern(day);
var localDateBefore = new DateTime(year, month, day, 0, 0, 0);
var beforeUtc = localDateBefore.LocalToUtc(tz);
var timeOnly = new TimeOnly(hour, minute);
// Act - shouldn't throw exceptions
var result = pattern.GetNextOccurrence(beforeUtc, timeOnly, tz);
// Assert - just make sure we got a valid result
Assert.NotEqual(DateTime.MaxValue, result);
// For invalid times (spring forward), should skip to next valid time
var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(result, tz);
if (month == 3) // Spring forward
{
// Should skip the invalid 2:30 AM time
Assert.True(resultLocal.Hour >= 3 || resultLocal > new DateTime(year, month, day));
}
}
[Fact]
public void MonthlyOrdinalRecurrencePattern_DST_FallBack_HandlesAmbiguousTimeCorrectly()
{
// This tests specifically the DST fall back case that was failing
var tz = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
var pattern = new MonthlyOrdinalRecurrencePattern(OrdinalDayOccurrence.First, DayOfWeek.Sunday);
// November 3, 2024 at 1:30 AM - First Sunday, falls on DST transition
var beforeDate = new DateTime(2024, 11, 2, 12, 0, 0);
var beforeUtc = beforeDate.LocalToUtc(tz);
var timeOnly = new TimeOnly(1, 30);
// Act
var result = pattern.GetNextOccurrence(beforeUtc, timeOnly, tz);
// Assert - should be November 3, 2024 at 1:30 AM
var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(result, tz);
Assert.Equal(new DateTime(2024, 11, 3, 1, 30, 0), resultLocal);
}
[Fact]
public void MonthlyRecurrence_MonthWithoutDay_SkipsToNextValidMonth()
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new MonthlyRecurrencePattern(31);
// February doesn't have 31 days
var februaryDate = new DateTime(2024, 2, 1, 12, 0, 0, DateTimeKind.Utc);
var timeOnly = new TimeOnly(12, 0);
// Act
var result = pattern.GetNextOccurrence(februaryDate, timeOnly, tz);
// Assert - should be March 31
Assert.Equal(new DateTime(2024, 3, 31, 12, 0, 0), result);
}
[Theory]
[InlineData("America/New_York", 2024, 11, 3, 1, 30)] // Fall back - ambiguous time
public void LocalToUtc_AmbiguousTime_HandlesConsistently(
string tzId, int year, int month, int day, int hour, int minute)
{
// Arrange
var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId);
var localTime = new DateTime(year, month, day, hour, minute, 0);
// Act
var result = localTime.LocalToUtc(tz);
// Assert - should be consistent, not testing exact value
Assert.Equal(DateTimeKind.Utc, result.Kind);
// Convert back should give either standard or DST time
var backToLocal = TimeZoneInfo.ConvertTimeFromUtc(result, tz);
Assert.Equal(new DateTime(year, month, day, hour, minute, 0), backToLocal);
}
}

View file

@ -4,11 +4,11 @@ public class DisplayHelpTopic : Packet
{
public DisplayHelpTopic(int topicID, bool display) : base(0xBF)
{
EnsureCapacity(11);
EnsureCapacity(11);
Stream.Write((short)0x17);
Stream.Write((byte)1);
Stream.Write(topicID);
Stream.Write(display);
}
}
Stream.Write((short)0x17);
Stream.Write((byte)1);
Stream.Write(topicID);
Stream.Write(display);
}
}

View file

@ -13,12 +13,12 @@ public class TestHelpTopicPacket
[InlineData(HelpTopic.EmptyingBowl, true)]
public void TestDisplayHelpTopic(int topic, bool display)
{
var expected = new DisplayHelpTopic(topic, display).Compile();
var expected = new DisplayHelpTopic(topic, display).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendDisplayHelpTopic(topic, display);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendDisplayHelpTopic(topic, display);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
}
}

View file

@ -6,22 +6,22 @@ public sealed class RaceChanger : Packet
{
public RaceChanger(bool female, Race targetRace) : base(0xBF)
{
EnsureCapacity(7);
EnsureCapacity(7);
Stream.Write((short)0x2A);
Stream.Write((byte)(female ? 1 : 0));
Stream.Write((byte)(targetRace.RaceID + 1));
}
Stream.Write((short)0x2A);
Stream.Write((byte)(female ? 1 : 0));
Stream.Write((byte)(targetRace.RaceID + 1));
}
}
public sealed class CloseRaceChanger : Packet
{
public CloseRaceChanger() : base(0xBF)
{
EnsureCapacity(7);
EnsureCapacity(7);
Stream.Write((short)0x2A);
Stream.Write((byte)0);
Stream.Write((byte)0xFF);
}
}
Stream.Write((short)0x2A);
Stream.Write((byte)0);
Stream.Write((byte)0xFF);
}
}

View file

@ -6,74 +6,74 @@ public sealed class PartyEmptyList : Packet
{
public PartyEmptyList(Serial m) : base(0xBF)
{
EnsureCapacity(7);
EnsureCapacity(7);
Stream.Write((short)0x0006);
Stream.Write((byte)0x02);
Stream.Write((byte)0);
Stream.Write(m);
}
Stream.Write((short)0x0006);
Stream.Write((byte)0x02);
Stream.Write((byte)0);
Stream.Write(m);
}
}
public sealed class PartyMemberList : Packet
{
public PartyMemberList(Party p) : base(0xBF)
{
EnsureCapacity(7 + p.Count * 4);
EnsureCapacity(7 + p.Count * 4);
Stream.Write((short)0x0006);
Stream.Write((byte)0x01);
Stream.Write((byte)p.Count);
Stream.Write((short)0x0006);
Stream.Write((byte)0x01);
Stream.Write((byte)p.Count);
for (var i = 0; i < p.Count; ++i)
{
Stream.Write(p[i].Mobile.Serial);
}
for (var i = 0; i < p.Count; ++i)
{
Stream.Write(p[i].Mobile.Serial);
}
}
}
public sealed class PartyRemoveMember : Packet
{
public PartyRemoveMember(Serial removed, Party p) : base(0xBF)
{
EnsureCapacity(11 + p.Count * 4);
EnsureCapacity(11 + p.Count * 4);
Stream.Write((short)0x0006);
Stream.Write((byte)0x02);
Stream.Write((byte)p.Count);
Stream.Write((short)0x0006);
Stream.Write((byte)0x02);
Stream.Write((byte)p.Count);
Stream.Write(removed);
Stream.Write(removed);
for (var i = 0; i < p.Count; ++i)
{
Stream.Write(p[i].Mobile.Serial);
}
for (var i = 0; i < p.Count; ++i)
{
Stream.Write(p[i].Mobile.Serial);
}
}
}
public sealed class PartyTextMessage : Packet
{
public PartyTextMessage(bool toAll, Serial from, string text) : base(0xBF)
{
text ??= "";
text ??= "";
EnsureCapacity(12 + text.Length * 2);
EnsureCapacity(12 + text.Length * 2);
Stream.Write((short)0x0006);
Stream.Write((byte)(toAll ? 0x04 : 0x03));
Stream.Write(from);
Stream.WriteBigUniNull(text);
}
Stream.Write((short)0x0006);
Stream.Write((byte)(toAll ? 0x04 : 0x03));
Stream.Write(from);
Stream.WriteBigUniNull(text);
}
}
public sealed class PartyInvitation : Packet
{
public PartyInvitation(Serial leader) : base(0xBF)
{
EnsureCapacity(10);
EnsureCapacity(10);
Stream.Write((short)0x0006);
Stream.Write((byte)0x07);
Stream.Write(leader);
}
}
Stream.Write((short)0x0006);
Stream.Write((byte)0x07);
Stream.Write(leader);
}
}

View file

@ -6,92 +6,93 @@ using Xunit;
namespace UOContent.Tests;
public class PartyPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential UOContent Tests")]
public class PartyPacketTests
{
[Fact]
public void TestPartyEmptyList()
{
Serial m = (Serial)0x1024u;
Serial m = (Serial)0x1024u;
var expected = new PartyEmptyList(m).Compile();
var expected = new PartyEmptyList(m).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendPartyRemoveMember(m);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendPartyRemoveMember(m);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Fact]
public void TestPartyRemoveMember()
{
var leader = new Mobile((Serial)0x1024u);
leader.DefaultMobileInit();
var leader = new Mobile((Serial)0x1024u);
leader.DefaultMobileInit();
var member = new Mobile((Serial)0x2048u);
member.DefaultMobileInit();
var member = new Mobile((Serial)0x2048u);
member.DefaultMobileInit();
var p = new Party(leader);
p.Add(member);
var p = new Party(leader);
p.Add(member);
var expected = new PartyRemoveMember(member.Serial, p).Compile();
var expected = new PartyRemoveMember(member.Serial, p).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendPartyRemoveMember(member.Serial, p);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendPartyRemoveMember(member.Serial, p);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Fact]
public void TestPartyMemberList()
{
var leader = new Mobile((Serial)0x1024u);
leader.DefaultMobileInit();
var leader = new Mobile((Serial)0x1024u);
leader.DefaultMobileInit();
var member = new Mobile((Serial)0x2048u);
member.DefaultMobileInit();
var member = new Mobile((Serial)0x2048u);
member.DefaultMobileInit();
var p = new Party(leader);
p.Add(member);
var p = new Party(leader);
p.Add(member);
var expected = new PartyMemberList(p).Compile();
var expected = new PartyMemberList(p).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendPartyMemberList(p);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendPartyMemberList(p);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void TestPartyTextMessage(bool toAll)
{
Serial serial = (Serial)0x1024u;
var text = "[Party] Stuff Happens";
Serial serial = (Serial)0x1024u;
var text = "[Party] Stuff Happens";
var expected = new PartyTextMessage(toAll, serial, text).Compile();
var expected = new PartyTextMessage(toAll, serial, text).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendPartyTextMessage(serial, text, toAll);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendPartyTextMessage(serial, text, toAll);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Fact]
public void TestPartyInvitation()
{
Serial m = (Serial)0x1024u;
Serial m = (Serial)0x1024u;
var expected = new PartyInvitation(m).Compile();
var expected = new PartyInvitation(m).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendPartyInvitation(m);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendPartyInvitation(m);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
}
}

View file

@ -13,12 +13,12 @@ public class CharacterStatuePacketTests
[InlineData(0x1024u, 1, 100, 200)]
public void TestSendStatueAnimation(uint s, int status, int anim, int frame)
{
var expected = new UpdateStatueAnimation((Serial)s, status, anim, frame).Compile();
var expected = new UpdateStatueAnimation((Serial)s, status, anim, frame).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendStatueAnimation((Serial)s, status, anim, frame);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendStatueAnimation((Serial)s, status, anim, frame);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
}
}

View file

@ -4,16 +4,16 @@ public class UpdateStatueAnimation : Packet
{
public UpdateStatueAnimation(Serial serial, int status, int animation, int frame) : base(0xBF, 17)
{
Stream.Write((short)0x11);
Stream.Write((short)0x19);
Stream.Write((byte)0x5);
Stream.Write(serial);
Stream.Write((byte)0);
Stream.Write((byte)0xFF);
Stream.Write((byte)status);
Stream.Write((byte)0);
Stream.Write((byte)animation);
Stream.Write((byte)0);
Stream.Write((byte)frame);
}
}
Stream.Write((short)0x11);
Stream.Write((short)0x19);
Stream.Write((byte)0x5);
Stream.Write(serial);
Stream.Write((byte)0);
Stream.Write((byte)0xFF);
Stream.Write((byte)status);
Stream.Write((byte)0);
Stream.Write((byte)animation);
Stream.Write((byte)0);
Stream.Write((byte)frame);
}
}

View file

@ -35,6 +35,6 @@ public class StaticLayoutTestGump : StaticGump<StaticLayoutTestGump>
protected override void BuildStrings(ref GumpStringsBuilder builder)
{
builder.SetStringSlot("petName", $"<CENTER>{_petName}</CENTER>");
builder.SetHtmlTextCentered("petName", _petName);
}
}

View file

@ -7,7 +7,7 @@ using Xunit;
namespace Server.Tests.Gumps;
[Collection("Sequential Tests")]
[Collection("Sequential UOContent Tests")]
public class TestLayoutGumps
{
[Fact]

View file

@ -15,78 +15,79 @@ public class TestBook : BaseBook
public TestBook(int itemID, string title, string author, int pageCount, bool writable) : base(itemID, title, author, pageCount, writable)
{
}
}
public TestBook(int itemID, bool writable) : base(itemID, writable)
{
}
}
public TestBook(Serial serial) : base(serial)
{
Pages = new BookPageInfo[20];
Pages = new BookPageInfo[20];
for (var i = 0; i < Pages.Length; ++i)
{
Pages[i] = new BookPageInfo();
}
for (var i = 0; i < Pages.Length; ++i)
{
Pages[i] = new BookPageInfo();
}
}
}
public class BookPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential UOContent Tests")]
public class BookPacketTests
{
[Theory]
[InlineData("🅵🅰🅽🅲🆈 🆃🅴🆇🆃 Author", "🅵🅰🅽🅲🆈 🆃🅴🆇🆃 Title")]
public void TestBookCover(string author, string title)
{
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
Serial serial = (Serial)0x1001;
var book = new TestBook(serial) { Author = author, Title = title };
Serial serial = (Serial)0x1001;
var book = new TestBook(serial) { Author = author, Title = title };
var expected = new BookHeader(m, book).Compile();
var expected = new BookHeader(m, book).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendBookCover(m, book);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendBookCover(m, book);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Fact]
public void TestBookContent()
{
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
Serial serial = (Serial)0x1001;
var book = new TestBook(serial) { Author = "Some Author", Title = "Some Title" };
book.Pages[0].Lines = new[]
{
"Some books start with actual content",
"This book does not have any actual content",
"Instead it has several pages of useless text"
};
Serial serial = (Serial)0x1001;
var book = new TestBook(serial) { Author = "Some Author", Title = "Some Title" };
book.Pages[0].Lines = new[]
{
"Some books start with actual content",
"This book does not have any actual content",
"Instead it has several pages of useless text"
};
book.Pages[1].Lines = new[]
{
"Another page exists but this page:",
"Has lots of: 🅵🅰🅽🅲🆈 🆃🅴🆇🆃",
"And just more: 🅵🅰🅽🅲🆈 🆃🅴🆇🆃",
"So everyone can read: 🅵🅰🅽🅲🆈 🆃🅴🆇🆃"
};
book.Pages[1].Lines = new[]
{
"Another page exists but this page:",
"Has lots of: 🅵🅰🅽🅲🆈 🆃🅴🆇🆃",
"And just more: 🅵🅰🅽🅲🆈 🆃🅴🆇🆃",
"So everyone can read: 🅵🅰🅽🅲🆈 🆃🅴🆇🆃"
};
book.Pages[2].Lines = new[]
{
"The end"
};
book.Pages[2].Lines = new[]
{
"The end"
};
var expected = new BookPageDetails(book).Compile();
var expected = new BookPageDetails(book).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendBookContent(book);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendBookContent(book);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
}

View file

@ -7,52 +7,52 @@ public sealed class BookPageDetails : Packet
{
public BookPageDetails(BaseBook book) : base(0x66)
{
EnsureCapacity(256);
EnsureCapacity(256);
Stream.Write(book.Serial);
Stream.Write((ushort)book.PagesCount);
Stream.Write(book.Serial);
Stream.Write((ushort)book.PagesCount);
for (var i = 0; i < book.PagesCount; ++i)
for (var i = 0; i < book.PagesCount; ++i)
{
var page = book.Pages[i];
Stream.Write((ushort)(i + 1));
Stream.Write((ushort)page.Lines.Length);
for (var j = 0; j < page.Lines.Length; ++j)
{
var page = book.Pages[i];
var buffer = page.Lines[j].GetBytesUtf8();
Stream.Write((ushort)(i + 1));
Stream.Write((ushort)page.Lines.Length);
for (var j = 0; j < page.Lines.Length; ++j)
{
var buffer = page.Lines[j].GetBytesUtf8();
Stream.Write(buffer, 0, buffer.Length);
Stream.Write((byte)0);
}
Stream.Write(buffer, 0, buffer.Length);
Stream.Write((byte)0);
}
}
}
}
public sealed class BookHeader : Packet
{
public BookHeader(Mobile from, BaseBook book) : base(0xD4)
{
var title = book.Title ?? "";
var author = book.Author ?? "";
var title = book.Title ?? "";
var author = book.Author ?? "";
var titleBuffer = title.GetBytesUtf8();
var authorBuffer = author.GetBytesUtf8();
var titleBuffer = title.GetBytesUtf8();
var authorBuffer = author.GetBytesUtf8();
EnsureCapacity(15 + titleBuffer.Length + authorBuffer.Length);
EnsureCapacity(15 + titleBuffer.Length + authorBuffer.Length);
Stream.Write(book.Serial);
Stream.Write(true);
Stream.Write(book.Writable && from.InRange(book.GetWorldLocation(), 1));
Stream.Write((ushort)book.PagesCount);
Stream.Write(book.Serial);
Stream.Write(true);
Stream.Write(book.Writable && from.InRange(book.GetWorldLocation(), 1));
Stream.Write((ushort)book.PagesCount);
Stream.Write((ushort)(titleBuffer.Length + 1));
Stream.Write(titleBuffer, 0, titleBuffer.Length);
Stream.Write((byte)0); // terminate
Stream.Write((ushort)(titleBuffer.Length + 1));
Stream.Write(titleBuffer, 0, titleBuffer.Length);
Stream.Write((byte)0); // terminate
Stream.Write((ushort)(authorBuffer.Length + 1));
Stream.Write(authorBuffer, 0, authorBuffer.Length);
Stream.Write((byte)0); // terminate
}
}
Stream.Write((ushort)(authorBuffer.Length + 1));
Stream.Write(authorBuffer, 0, authorBuffer.Length);
Stream.Write((byte)0); // terminate
}
}

View file

@ -7,8 +7,8 @@ using Xunit;
namespace UOContent.Tests;
[Collection("Sequential Tests")]
public class BulletinBoardPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential UOContent Tests")]
public class BulletinBoardPacketTests
{
[Theory]
[InlineData("Test Name")]
@ -16,16 +16,16 @@ public class BulletinBoardPacketTests : IClassFixture<ServerFixture>
[InlineData("🅵🅰🅽🅲🆈 🆃🅴🆇🆃")]
public void TestSendBBDisplayBoard(string boardName)
{
var bb = new TestBulletinBoard(0x234) { BoardName = boardName };
var bb = new TestBulletinBoard(0x234) { BoardName = boardName };
var expected = new BBDisplayBoard(bb).Compile();
var expected = new BBDisplayBoard(bb).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendBBDisplayBoard(bb);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendBBDisplayBoard(bb);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Theory]
[InlineData("The Subject", false, "First Line", "Second Line", "Third Line")]
@ -36,32 +36,32 @@ public class BulletinBoardPacketTests : IClassFixture<ServerFixture>
[InlineData("🅵🅰🅽🅲🆈 🆃🅴🆇🆃", true, "First Line", "Second Line")]
public void TestSendBBHeaderMessage(string subject, bool content, params string[] lines)
{
var poster = new Mobile((Serial)0x1024u) { Name = "Kamron" };
poster.DefaultMobileInit();
var poster = new Mobile((Serial)0x1024u) { Name = "Kamron" };
poster.DefaultMobileInit();
var bb = new TestBulletinBoard(0x234);
bb.PostMessage(poster, null, subject, lines);
var bb = new TestBulletinBoard(0x234);
bb.PostMessage(poster, null, subject, lines);
var msg = bb.Items[0] as BulletinMessage;
var msg = bb.Items[0] as BulletinMessage;
var expected = (content ?
(Packet)new BBMessageContent(bb, msg) : new BBMessageHeader(bb, msg)).Compile();
var expected = (content ?
(Packet)new BBMessageContent(bb, msg) : new BBMessageHeader(bb, msg)).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendBBMessage(bb, msg, content);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendBBMessage(bb, msg, content);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
}
internal class TestBulletinBoard : BaseBulletinBoard
{
public TestBulletinBoard(int itemID) : base(itemID)
{
}
}
public TestBulletinBoard(Serial serial) : base(serial)
{
}
}
}
}

View file

@ -1,149 +1,148 @@
using Server.Items;
using Server.Text;
namespace Server.Network
namespace Server.Network;
public class BBDisplayBoard : Packet
{
public class BBDisplayBoard : Packet
public BBDisplayBoard(BaseBulletinBoard board) : base(0x71)
{
public BBDisplayBoard(BaseBulletinBoard board) : base(0x71)
EnsureCapacity(38);
var buffer = (board.BoardName ?? "").GetBytesUtf8();
Stream.Write((byte)0x00); // Packet ID
Stream.Write(board.Serial); // Bulletin board serial
// Bulletin board name
if (buffer.Length >= 29)
{
EnsureCapacity(38);
var buffer = (board.BoardName ?? "").GetBytesUtf8();
Stream.Write((byte)0x00); // Packet ID
Stream.Write(board.Serial); // Bulletin board serial
// Bulletin board name
if (buffer.Length >= 29)
{
Stream.Write(buffer, 0, 29);
Stream.Write((byte)0);
}
else
{
Stream.Write(buffer, 0, buffer.Length);
Stream.Fill(30 - buffer.Length);
}
}
}
public class BBMessageHeader : Packet
{
public BBMessageHeader(BaseBulletinBoard board, BulletinMessage msg) : base(0x71)
{
var poster = SafeString(msg.PostedName);
var subject = SafeString(msg.Subject);
var time = SafeString(msg.GetTimeAsString());
EnsureCapacity(22 + poster.Length + subject.Length + time.Length);
Stream.Write((byte)0x01); // Packet ID
Stream.Write(board.Serial); // Bulletin board serial
Stream.Write(msg.Serial); // Message serial
Stream.Write(msg.Thread?.Serial ?? Serial.Zero); // Thread serial--parent
WriteString(poster);
WriteString(subject);
WriteString(time);
}
public void WriteString(string v)
{
var buffer = v.GetBytesUtf8();
var len = buffer.Length + 1;
if (len > 255)
{
len = 255;
}
Stream.Write((byte)len);
Stream.Write(buffer, 0, len - 1);
Stream.Write(buffer, 0, 29);
Stream.Write((byte)0);
}
public string SafeString(string v) => v ?? string.Empty;
}
public class BBMessageContent : Packet
{
public BBMessageContent(BaseBulletinBoard board, BulletinMessage msg) : base(0x71)
else
{
var poster = SafeString(msg.PostedName);
var subject = SafeString(msg.Subject);
var time = SafeString(msg.GetTimeAsString());
EnsureCapacity(22 + poster.Length + subject.Length + time.Length);
Stream.Write((byte)0x02); // Packet ID
Stream.Write(board.Serial); // Bulletin board serial
Stream.Write(msg.Serial); // Message serial
WriteString(poster);
WriteString(subject);
WriteString(time);
Stream.Write((short)msg.PostedBody);
Stream.Write((short)msg.PostedHue);
var len = msg.PostedEquip.Length;
if (len > 255)
{
len = 255;
}
Stream.Write((byte)len);
for (var i = 0; i < len; ++i)
{
var eq = msg.PostedEquip[i];
Stream.Write((short)eq._itemID);
Stream.Write((short)eq._hue);
}
len = msg.Lines.Length;
if (len > 255)
{
len = 255;
}
Stream.Write((byte)len);
for (var i = 0; i < len; ++i)
{
WriteString(msg.Lines[i], true);
}
Stream.Write(buffer, 0, buffer.Length);
Stream.Fill(30 - buffer.Length);
}
public void WriteString(string v, bool padding = false)
{
var buffer = v.GetBytesUtf8();
var tail = padding ? 2 : 1;
var len = buffer.Length + tail;
if (len > 255)
{
len = 255;
}
Stream.Write((byte)len);
Stream.Write(buffer, 0, len - tail);
if (padding)
{
Stream.Write((short)0); // padding compensates for a client bug
}
else
{
Stream.Write((byte)0);
}
}
public string SafeString(string v) => v ?? string.Empty;
}
}
public class BBMessageHeader : Packet
{
public BBMessageHeader(BaseBulletinBoard board, BulletinMessage msg) : base(0x71)
{
var poster = SafeString(msg.PostedName);
var subject = SafeString(msg.Subject);
var time = SafeString(msg.GetTimeAsString());
EnsureCapacity(22 + poster.Length + subject.Length + time.Length);
Stream.Write((byte)0x01); // Packet ID
Stream.Write(board.Serial); // Bulletin board serial
Stream.Write(msg.Serial); // Message serial
Stream.Write(msg.Thread?.Serial ?? Serial.Zero); // Thread serial--parent
WriteString(poster);
WriteString(subject);
WriteString(time);
}
public void WriteString(string v)
{
var buffer = v.GetBytesUtf8();
var len = buffer.Length + 1;
if (len > 255)
{
len = 255;
}
Stream.Write((byte)len);
Stream.Write(buffer, 0, len - 1);
Stream.Write((byte)0);
}
public string SafeString(string v) => v ?? string.Empty;
}
public class BBMessageContent : Packet
{
public BBMessageContent(BaseBulletinBoard board, BulletinMessage msg) : base(0x71)
{
var poster = SafeString(msg.PostedName);
var subject = SafeString(msg.Subject);
var time = SafeString(msg.GetTimeAsString());
EnsureCapacity(22 + poster.Length + subject.Length + time.Length);
Stream.Write((byte)0x02); // Packet ID
Stream.Write(board.Serial); // Bulletin board serial
Stream.Write(msg.Serial); // Message serial
WriteString(poster);
WriteString(subject);
WriteString(time);
Stream.Write((short)msg.PostedBody);
Stream.Write((short)msg.PostedHue);
var len = msg.PostedEquip.Length;
if (len > 255)
{
len = 255;
}
Stream.Write((byte)len);
for (var i = 0; i < len; ++i)
{
var eq = msg.PostedEquip[i];
Stream.Write((short)eq._itemID);
Stream.Write((short)eq._hue);
}
len = msg.Lines.Length;
if (len > 255)
{
len = 255;
}
Stream.Write((byte)len);
for (var i = 0; i < len; ++i)
{
WriteString(msg.Lines[i], true);
}
}
public void WriteString(string v, bool padding = false)
{
var buffer = v.GetBytesUtf8();
var tail = padding ? 2 : 1;
var len = buffer.Length + tail;
if (len > 255)
{
len = 255;
}
Stream.Write((byte)len);
Stream.Write(buffer, 0, len - tail);
if (padding)
{
Stream.Write((short)0); // padding compensates for a client bug
}
else
{
Stream.Write((byte)0);
}
}
public string SafeString(string v) => v ?? string.Empty;
}

View file

@ -6,42 +6,42 @@ using Xunit;
namespace UOContent.Tests;
[Collection("Sequential Tests")]
public class MahjongPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential UOContent Tests")]
public class MahjongPacketTests
{
[Fact]
public void TestMahjongJoinGame()
{
Serial game = (Serial)0x1024u;
Serial game = (Serial)0x1024u;
var expected = new MahjongJoinGame(game).Compile();
var expected = new MahjongJoinGame(game).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongJoinGame(game);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongJoinGame(game);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void TestMahjongPlayersInfo(bool showScores)
{
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var game = new MahjongGame { ShowScores = showScores };
game.Players.Join(m);
var game = new MahjongGame { ShowScores = showScores };
game.Players.Join(m);
var expected = new MahjongPlayersInfo(game, m).Compile();
var expected = new MahjongPlayersInfo(game, m).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongPlayersInfo(game, m);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongPlayersInfo(game, m);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Theory]
[InlineData(true, true)]
@ -50,68 +50,68 @@ public class MahjongPacketTests : IClassFixture<ServerFixture>
[InlineData(false, false)]
public void TestMahjongGeneralInfo(bool showScores, bool spectatorVision)
{
var game = new MahjongGame { ShowScores = showScores, SpectatorVision = spectatorVision};
var game = new MahjongGame { ShowScores = showScores, SpectatorVision = spectatorVision};
var expected = new MahjongGeneralInfo(game).Compile();
var expected = new MahjongGeneralInfo(game).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongGeneralInfo(game);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongGeneralInfo(game);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void TestMahjongTilesInfo(bool spectatorVision)
{
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var game = new MahjongGame { SpectatorVision = spectatorVision };
game.Players.Join(m);
var game = new MahjongGame { SpectatorVision = spectatorVision };
game.Players.Join(m);
var expected = new MahjongTilesInfo(game, m).Compile();
var expected = new MahjongTilesInfo(game, m).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongTilesInfo(game, m);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongTilesInfo(game, m);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void TestMahjongTileInfo(bool spectatorVision)
{
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var game = new MahjongGame { SpectatorVision = spectatorVision };
game.Players.Join(m);
var game = new MahjongGame { SpectatorVision = spectatorVision };
game.Players.Join(m);
var expected = new MahjongTileInfo(game.Tiles[0], m).Compile();
var expected = new MahjongTileInfo(game.Tiles[0], m).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongTileInfo(game.Tiles[0], m);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongTileInfo(game.Tiles[0], m);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Fact]
public void TestMahjongRelieve()
{
Serial game = (Serial)0x1024u;
Serial game = (Serial)0x1024u;
var expected = new MahjongRelieve(game).Compile();
var expected = new MahjongRelieve(game).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongRelieve(game);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMahjongRelieve(game);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
}
}

View file

@ -7,176 +7,132 @@ public sealed class MahjongJoinGame : Packet
{
public MahjongJoinGame(Serial game) : base(0xDA)
{
EnsureCapacity(9);
EnsureCapacity(9);
Stream.Write(game);
Stream.Write((byte)0);
Stream.Write((byte)0x19);
}
Stream.Write(game);
Stream.Write((byte)0);
Stream.Write((byte)0x19);
}
}
public sealed class MahjongPlayersInfo : Packet
{
public MahjongPlayersInfo(MahjongGame game, Mobile to) : base(0xDA)
{
var players = game.Players;
var players = game.Players;
EnsureCapacity(11 + 45 * players.Seats);
EnsureCapacity(11 + 45 * players.Seats);
Stream.Write(game.Serial);
Stream.Write((byte)0);
Stream.Write((byte)0x2);
Stream.Write(game.Serial);
Stream.Write((byte)0);
Stream.Write((byte)0x2);
Stream.Write((byte)0);
Stream.Write((byte)players.Seats);
Stream.Write((byte)0);
Stream.Write((byte)players.Seats);
var n = 0;
for (var i = 0; i < players.Seats; i++)
var n = 0;
for (var i = 0; i < players.Seats; i++)
{
var mobile = players.GetPlayer(i);
if (mobile != null)
{
var mobile = players.GetPlayer(i);
Stream.Write(mobile.Serial);
Stream.Write(players.DealerPosition == i ? (byte)0x1 : (byte)0x2);
Stream.Write((byte)i);
if (mobile != null)
if (game.ShowScores || mobile == to)
{
Stream.Write(mobile.Serial);
Stream.Write(players.DealerPosition == i ? (byte)0x1 : (byte)0x2);
Stream.Write((byte)i);
if (game.ShowScores || mobile == to)
{
Stream.Write(players.GetScore(i));
}
else
{
Stream.Write(0);
}
Stream.Write((short)0);
Stream.Write((byte)0);
Stream.Write(players.IsPublic(i));
Stream.WriteAsciiFixed(mobile.Name, 30);
Stream.Write(!players.IsInGamePlayer(i));
n++;
Stream.Write(players.GetScore(i));
}
else if (game.ShowScores)
else
{
Stream.Write(0);
Stream.Write((byte)0x2);
Stream.Write((byte)i);
Stream.Write(players.GetScore(i));
Stream.Write((short)0);
Stream.Write((byte)0);
Stream.Write(players.IsPublic(i));
Stream.WriteAsciiFixed("", 30);
Stream.Write(true);
n++;
}
}
if (n != players.Seats)
Stream.Write((short)0);
Stream.Write((byte)0);
Stream.Write(players.IsPublic(i));
Stream.WriteAsciiFixed(mobile.Name, 30);
Stream.Write(!players.IsInGamePlayer(i));
n++;
}
else if (game.ShowScores)
{
Stream.Seek(10, SeekOrigin.Begin);
Stream.Write((byte)n);
Stream.Write(0);
Stream.Write((byte)0x2);
Stream.Write((byte)i);
Stream.Write(players.GetScore(i));
Stream.Write((short)0);
Stream.Write((byte)0);
Stream.Write(players.IsPublic(i));
Stream.WriteAsciiFixed("", 30);
Stream.Write(true);
n++;
}
}
if (n != players.Seats)
{
Stream.Seek(10, SeekOrigin.Begin);
Stream.Write((byte)n);
}
}
}
public sealed class MahjongGeneralInfo : Packet
{
public MahjongGeneralInfo(MahjongGame game) : base(0xDA)
{
EnsureCapacity(13);
EnsureCapacity(13);
Stream.Write(game.Serial);
Stream.Write((byte)0);
Stream.Write((byte)0x5);
Stream.Write(game.Serial);
Stream.Write((byte)0);
Stream.Write((byte)0x5);
Stream.Write((short)0);
Stream.Write((byte)0);
Stream.Write((short)0);
Stream.Write((byte)0);
Stream.Write((byte)((game.ShowScores ? 0x1 : 0x0) | (game.SpectatorVision ? 0x2 : 0x0)));
Stream.Write((byte)((game.ShowScores ? 0x1 : 0x0) | (game.SpectatorVision ? 0x2 : 0x0)));
Stream.Write((byte)game.Dices.First);
Stream.Write((byte)game.Dices.Second);
Stream.Write((byte)game.Dices.First);
Stream.Write((byte)game.Dices.Second);
Stream.Write((byte)game.DealerIndicator.Wind);
Stream.Write((short)game.DealerIndicator.Position.Y);
Stream.Write((short)game.DealerIndicator.Position.X);
Stream.Write((byte)game.DealerIndicator.Direction);
Stream.Write((byte)game.DealerIndicator.Wind);
Stream.Write((short)game.DealerIndicator.Position.Y);
Stream.Write((short)game.DealerIndicator.Position.X);
Stream.Write((byte)game.DealerIndicator.Direction);
Stream.Write((short)game.WallBreakIndicator.Position.Y);
Stream.Write((short)game.WallBreakIndicator.Position.X);
}
Stream.Write((short)game.WallBreakIndicator.Position.Y);
Stream.Write((short)game.WallBreakIndicator.Position.X);
}
}
public sealed class MahjongTilesInfo : Packet
{
public MahjongTilesInfo(MahjongGame game, Mobile to) : base(0xDA)
{
var tiles = game.Tiles;
var players = game.Players;
var tiles = game.Tiles;
var players = game.Players;
EnsureCapacity(11 + 9 * tiles.Length);
EnsureCapacity(11 + 9 * tiles.Length);
Stream.Write(game.Serial);
Stream.Write((byte)0);
Stream.Write((byte)0x4);
Stream.Write(game.Serial);
Stream.Write((byte)0);
Stream.Write((byte)0x4);
Stream.Write((short)tiles.Length);
foreach (var tile in tiles)
{
Stream.Write((byte)tile.Number);
if (tile.Flipped)
{
var hand = tile.Dimensions.GetHandArea();
if (hand < 0 || players.IsPublic(hand) || players.GetPlayer(hand) == to ||
game.SpectatorVision && players.IsSpectator(to))
{
Stream.Write((byte)tile.Value);
}
else
{
Stream.Write((byte)0);
}
}
else
{
Stream.Write((byte)0);
}
Stream.Write((short)tile.Position.Y);
Stream.Write((short)tile.Position.X);
Stream.Write((byte)tile.StackLevel);
Stream.Write((byte)tile.Direction);
Stream.Write(tile.Flipped ? (byte)0x10 : (byte)0x0);
}
}
}
public sealed class MahjongTileInfo : Packet
{
public MahjongTileInfo(MahjongTile tile, Mobile to) : base(0xDA)
{
var game = tile.Game;
var players = game.Players;
EnsureCapacity(18);
Stream.Write(tile.Game.Serial);
Stream.Write((byte)0);
Stream.Write((byte)0x3);
Stream.Write((short)tiles.Length);
foreach (var tile in tiles)
{
Stream.Write((byte)tile.Number);
if (tile.Flipped)
@ -205,16 +161,60 @@ public sealed class MahjongTileInfo : Packet
Stream.Write(tile.Flipped ? (byte)0x10 : (byte)0x0);
}
}
}
public sealed class MahjongTileInfo : Packet
{
public MahjongTileInfo(MahjongTile tile, Mobile to) : base(0xDA)
{
var game = tile.Game;
var players = game.Players;
EnsureCapacity(18);
Stream.Write(tile.Game.Serial);
Stream.Write((byte)0);
Stream.Write((byte)0x3);
Stream.Write((byte)tile.Number);
if (tile.Flipped)
{
var hand = tile.Dimensions.GetHandArea();
if (hand < 0 || players.IsPublic(hand) || players.GetPlayer(hand) == to ||
game.SpectatorVision && players.IsSpectator(to))
{
Stream.Write((byte)tile.Value);
}
else
{
Stream.Write((byte)0);
}
}
else
{
Stream.Write((byte)0);
}
Stream.Write((short)tile.Position.Y);
Stream.Write((short)tile.Position.X);
Stream.Write((byte)tile.StackLevel);
Stream.Write((byte)tile.Direction);
Stream.Write(tile.Flipped ? (byte)0x10 : (byte)0x0);
}
}
public sealed class MahjongRelieve : Packet
{
public MahjongRelieve(Serial game) : base(0xDA)
{
EnsureCapacity(9);
EnsureCapacity(9);
Stream.Write(game);
Stream.Write((byte)0);
Stream.Write((byte)0x1A);
}
}
Stream.Write(game);
Stream.Write((byte)0);
Stream.Write((byte)0x1A);
}
}

View file

@ -6,62 +6,62 @@ public sealed class MapDetails : Packet
{
public MapDetails(MapItem map) : base(0x90, 19)
{
Stream.Write(map.Serial);
Stream.Write((short)0x139D);
Stream.Write((short)map.Bounds.Start.X);
Stream.Write((short)map.Bounds.Start.Y);
Stream.Write((short)map.Bounds.End.X);
Stream.Write((short)map.Bounds.End.Y);
Stream.Write((short)map.Width);
Stream.Write((short)map.Height);
}
Stream.Write(map.Serial);
Stream.Write((short)0x139D);
Stream.Write((short)map.Bounds.Start.X);
Stream.Write((short)map.Bounds.Start.Y);
Stream.Write((short)map.Bounds.End.X);
Stream.Write((short)map.Bounds.End.Y);
Stream.Write((short)map.Width);
Stream.Write((short)map.Height);
}
}
public sealed class MapDetailsNew : Packet
{
public MapDetailsNew(MapItem map) : base(0xF5, 21)
{
Stream.Write(map.Serial);
Stream.Write((short)0x139D);
Stream.Write((short)map.Bounds.Start.X);
Stream.Write((short)map.Bounds.Start.Y);
Stream.Write((short)map.Bounds.End.X);
Stream.Write((short)map.Bounds.End.Y);
Stream.Write((short)map.Width);
Stream.Write((short)map.Height);
Stream.Write((short)(map.Facet?.MapID ?? 0));
}
Stream.Write(map.Serial);
Stream.Write((short)0x139D);
Stream.Write((short)map.Bounds.Start.X);
Stream.Write((short)map.Bounds.Start.Y);
Stream.Write((short)map.Bounds.End.X);
Stream.Write((short)map.Bounds.End.Y);
Stream.Write((short)map.Width);
Stream.Write((short)map.Height);
Stream.Write((short)(map.Facet?.MapID ?? 0));
}
}
public class MapCommand : Packet
{
public MapCommand(MapItem map, int command, int number, int x, int y) : base(0x56, 11)
{
Stream.Write(map.Serial);
Stream.Write((byte)command);
Stream.Write((byte)number);
Stream.Write((short)x);
Stream.Write((short)y);
}
Stream.Write(map.Serial);
Stream.Write((byte)command);
Stream.Write((byte)number);
Stream.Write((short)x);
Stream.Write((short)y);
}
}
public sealed class MapDisplay : MapCommand
{
public MapDisplay(MapItem map) : base(map, 5, 0, 0, 0)
{
}
}
}
public sealed class MapAddPin : MapCommand
{
public MapAddPin(MapItem map, Point2D point) : base(map, 1, 0, point.X, point.Y)
{
}
}
}
public sealed class MapSetEditable : MapCommand
{
public MapSetEditable(MapItem map, bool editable) : base(map, 7, editable ? 1 : 0, 0, 0)
{
}
}
}
}

View file

@ -7,26 +7,26 @@ using Xunit;
namespace UOContent.Tests;
[Collection("Sequential Tests")]
public class TestMapItemPackets : IClassFixture<ServerFixture>
[Collection("Sequential UOContent Tests")]
public class TestMapItemPackets
{
[Theory]
[InlineData(ProtocolChanges.NewCharacterList)]
[InlineData(ProtocolChanges.None)]
public void TestSendMapDetails(ProtocolChanges changes)
{
var mapItem = new MapItem(Map.Trammel);
var mapItem = new MapItem(Map.Trammel);
var ns = PacketTestUtilities.CreateTestNetState();
ns.ProtocolChanges = changes;
var ns = PacketTestUtilities.CreateTestNetState();
ns.ProtocolChanges = changes;
var expected = (ns.NewCharacterList ?
(Packet)new MapDetailsNew(mapItem) : new MapDetails(mapItem)).Compile();
ns.SendMapDetails(mapItem);
var expected = (ns.NewCharacterList ?
(Packet)new MapDetailsNew(mapItem) : new MapDetails(mapItem)).Compile();
ns.SendMapDetails(mapItem);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Theory]
[InlineData(5, 0, 0, 0)]
@ -35,14 +35,14 @@ public class TestMapItemPackets : IClassFixture<ServerFixture>
[InlineData(7, 0, 0, 0)]
public void TestSendMapCommand(int command, int number, int x, int y)
{
var mapItem = new MapItem(Map.Trammel);
var mapItem = new MapItem(Map.Trammel);
var expected = new MapCommand(mapItem, command, number, x, y).Compile();
var expected = new MapCommand(mapItem, command, number, x, y).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMapCommand(mapItem, command, x, y, number > 0);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendMapCommand(mapItem, command, x, y, number > 0);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
}
}

View file

@ -7,50 +7,50 @@ using Xunit;
namespace UOContent.Tests;
[Collection("Sequential Tests")]
public class CorpsePacketTests : IClassFixture<ServerFixture>
[Collection("Sequential UOContent Tests")]
public class CorpsePacketTests
{
[Fact]
public void TestCorpseEquipPacket()
{
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var weapon = new VikingSword();
m.EquipItem(weapon);
var weapon = new VikingSword();
m.EquipItem(weapon);
var c = new Corpse(m, m.Items);
var c = new Corpse(m, m.Items);
var expected = new CorpseEquip(m, c).Compile();
var expected = new CorpseEquip(m, c).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendCorpseEquip(m, c);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendCorpseEquip(m, c);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Theory]
[InlineData(ProtocolChanges.None)]
[InlineData(ProtocolChanges.ContainerGridLines)]
public void TestCorpseContainerPacket(ProtocolChanges changes)
{
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var m = new Mobile((Serial)0x1);
m.DefaultMobileInit();
var weapon = new VikingSword();
m.EquipItem(weapon);
var weapon = new VikingSword();
m.EquipItem(weapon);
var c = new Corpse(m, m.Items);
var c = new Corpse(m, m.Items);
var ns = PacketTestUtilities.CreateTestNetState();
ns.ProtocolChanges = changes;
var ns = PacketTestUtilities.CreateTestNetState();
ns.ProtocolChanges = changes;
var expected = (ns.ContainerGridLines ? (Packet)new CorpseContent6017(m, c) : new CorpseContent(m, c)).Compile();
var expected = (ns.ContainerGridLines ? (Packet)new CorpseContent6017(m, c) : new CorpseContent(m, c)).Compile();
ns.SendCorpseContent(m, c);
ns.SendCorpseContent(m, c);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
}
}

View file

@ -14,24 +14,24 @@ public class WeaponAbilityPacketTests
[InlineData(1000, false)]
public void TestSpecialAbility(int abilityId, bool active)
{
var expected = new ToggleSpecialAbility(abilityId, active).Compile();
var expected = new ToggleSpecialAbility(abilityId, active).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendToggleSpecialAbility(abilityId, active);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendToggleSpecialAbility(abilityId, active);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Fact]
public void TestClearAbility()
{
var expected = new ClearWeaponAbility().Compile();
var expected = new ClearWeaponAbility().Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendClearWeaponAbility();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendClearWeaponAbility();
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
}
}

View file

@ -6,13 +6,13 @@ public sealed class ToggleSpecialAbility : Packet
{
public ToggleSpecialAbility(int abilityID, bool active) : base(0xBF)
{
EnsureCapacity(7);
EnsureCapacity(7);
Stream.Write((short)0x25);
Stream.Write((short)0x25);
Stream.Write((short)abilityID);
Stream.Write(active);
}
Stream.Write((short)abilityID);
Stream.Write(active);
}
}
public sealed class ClearWeaponAbility : Packet
@ -21,8 +21,8 @@ public sealed class ClearWeaponAbility : Packet
public ClearWeaponAbility() : base(0xBF)
{
EnsureCapacity(5);
EnsureCapacity(5);
Stream.Write((short)0x21);
}
}
Stream.Write((short)0x21);
}
}

View file

@ -0,0 +1,137 @@
using System.Buffers;
using Server.Misc;
using Xunit;
namespace Server.Tests;
public class NameVerificationTests
{
[Theory]
[InlineData("John")]
[InlineData("Mary Ann")]
[InlineData("Bob-Jones")]
[InlineData("O'Malley")]
public void ValidatePlayerName_ValidNames_ReturnsTrue(string name)
{
Assert.True(NameVerification.ValidatePlayerName(name));
}
[Theory]
[InlineData("Rex")]
[InlineData("MrWhiskers")]
[InlineData("Fido")]
public void ValidatePetName_ValidNames_ReturnsTrue(string name)
{
Assert.True(NameVerification.ValidatePetName(name));
}
[Theory]
[InlineData("Mrs-Smith")]
[InlineData("Mr. Whiskers")]
[InlineData("Dog123")]
public void ValidatePetName_InvalidNames_ReturnsFalse(string name)
{
Assert.False(NameVerification.ValidatePetName(name));
}
[Theory]
[InlineData("Blacksmith12")]
[InlineData("Baker")]
[InlineData("Innkeeper")]
public void ValidateVendorName_ValidNames_ReturnsTrue(string name)
{
Assert.True(NameVerification.ValidateVendorName(name));
}
[Theory]
[InlineData("Blacksmith 123")]
[InlineData("Baker-Smith")]
[InlineData("*Innkeeper*")]
public void ValidateVendorName_ValidNames_ReturnsFalse(string name)
{
Assert.False(NameVerification.ValidateVendorName(name));
}
[Fact]
public void Validate_MinimumLengthName_ReturnsTrue()
{
Assert.True(NameVerification.Validate("Ab", 2, 16, true, false));
}
[Fact]
public void Validate_MaximumLengthName_ReturnsTrue()
{
Assert.True(NameVerification.Validate("AbcdefghijklmnopqrsT", 1, 20, true, true));
}
[Fact]
public void Validate_MaxExceptions_ReturnsTrue()
{
var exceptions = SearchValues.Create(' ', '-', '.');
Assert.True(NameVerification.Validate("A-B.C D", 2, 16, true, false, false, 3, exceptions));
}
[Fact]
public void Validate_BoundaryOfDisallowedWord_ReturnsTrueWhenNotActuallyDisallowed()
{
// "ass" is in disallowed, but "class" has proper boundaries
Assert.True(NameVerification.ValidatePlayerName("Class"));
}
// Negative Tests
[Fact]
public void Validate_EmptyName_ReturnsFalse()
{
Assert.False(NameVerification.Validate("", 1, 20, true, true));
}
[Fact]
public void Validate_TooShortName_ReturnsFalse()
{
Assert.False(NameVerification.Validate("A", 2, 16, true, false));
}
[Fact]
public void Validate_TooLongName_ReturnsFalse()
{
Assert.False(NameVerification.Validate("AbcdefghijklmnopqrstuvwxyzABCDEF", 2, 16, true, false));
}
[Theory]
[InlineData("ass")]
[InlineData("GodDamn Fine")]
[InlineData("Fuck")]
public void Validate_DisallowedWords_ReturnsFalse(string name)
{
Assert.False(NameVerification.ValidatePlayerName(name));
}
[Theory]
[InlineData("GMJohn")]
[InlineData("LordBob")]
[InlineData("SeerMagic")]
public void Validate_DisallowedPrefixes_ReturnsFalse(string name)
{
Assert.False(NameVerification.ValidatePlayerName(name));
}
[Fact]
public void Validate_TooManyExceptions_ReturnsFalse()
{
var exceptions = SearchValues.Create(' ', '-', '.');
Assert.False(NameVerification.Validate("A-B.C D-E", 2, 16, true, false, false, 3, exceptions));
}
[Fact]
public void Validate_ExceptionAtStartWhenNotAllowed_ReturnsFalse()
{
var exceptions = SearchValues.Create(' ', '-', '.');
Assert.False(NameVerification.Validate("-John", 2, 16, true, false, true, 1, exceptions));
}
[Fact]
public void Validate_DisallowedCharacters_ReturnsFalse()
{
Assert.False(NameVerification.Validate("John123", 2, 16, true, false));
}
}

View file

@ -0,0 +1,53 @@
using Server.Misc;
using Xunit;
namespace Server.Tests;
public class ProfanityProtectionTests
{
[Theory]
[InlineData("Hello world")]
[InlineData("This is a normal conversation")]
[InlineData("I would like to trade with you")]
public void Speech_WithoutProfanity_PassesValidation(string speech)
{
Assert.False(ProfanityProtection.ContainsProfanity(speech));
}
[Fact]
public void Speech_Empty_PassesValidation()
{
Assert.False(ProfanityProtection.ContainsProfanity(""));
}
[Theory]
[InlineData("I'm going to class tomorrow")] // contains "ass" but in "class"
[InlineData("This assignment is hard")] // contains "ass" but in "assignment"
[InlineData("That's a nice cocktail")] // contains "cock" but in "cocktail"
public void Speech_WithWordsThatLookLikeProfanity_PassesValidation(string speech)
{
Assert.False(ProfanityProtection.ContainsProfanity(speech));
}
[Theory]
[InlineData("This is ass")]
[InlineData("What the fuck")]
[InlineData("You're a bitch")]
public void Speech_WithProfanity_FailsValidation(string speech)
{
Assert.True(ProfanityProtection.ContainsProfanity(speech));
}
[Theory]
[InlineData("ass")] // standalone profanity
[InlineData("an ass joke")] // profanity with word boundaries
[InlineData("ass.")] // profanity followed by punctuation
public void ContainsDisallowedWord_DetectsProfanityWithBoundaries(string speech)
{
Assert.True(NameVerification.ContainsDisallowedWord(
speech,
ProfanityProtection.Disallowed,
ProfanityProtection.DisallowedSearchValues
));
}
}

View file

@ -11,7 +11,8 @@ using Xunit;
namespace UOContent.Tests;
public class BoatPacketTests : IClassFixture<ServerFixture>
[Collection("Sequential UOContent Tests")]
public class BoatPacketTests
{
[Theory]
[InlineData(Direction.West, 10, 100, 200)]

View file

@ -15,117 +15,117 @@ public sealed class MoveBoatHS : Packet
int yOffset
) : base(0xF6)
{
EnsureCapacity(3 + 15 + ents.Count * 10);
EnsureCapacity(3 + 15 + ents.Count * 10);
Stream.Write(boat.Serial);
Stream.Write((byte)speed);
Stream.Write((byte)d);
Stream.Write((byte)boat.Facing);
Stream.Write((short)(boat.X + xOffset));
Stream.Write((short)(boat.Y + yOffset));
Stream.Write((short)boat.Z);
Stream.Write((short)0); // count placeholder
Stream.Write(boat.Serial);
Stream.Write((byte)speed);
Stream.Write((byte)d);
Stream.Write((byte)boat.Facing);
Stream.Write((short)(boat.X + xOffset));
Stream.Write((short)(boat.Y + yOffset));
Stream.Write((short)boat.Z);
Stream.Write((short)0); // count placeholder
var count = 0;
var count = 0;
foreach (var ent in ents)
{
Stream.Write(ent.Serial);
Stream.Write((short)(ent.X + xOffset));
Stream.Write((short)(ent.Y + yOffset));
Stream.Write((short)ent.Z);
++count;
}
Stream.Seek(16, SeekOrigin.Begin);
Stream.Write((short)count);
foreach (var ent in ents)
{
Stream.Write(ent.Serial);
Stream.Write((short)(ent.X + xOffset));
Stream.Write((short)(ent.Y + yOffset));
Stream.Write((short)ent.Z);
++count;
}
Stream.Seek(16, SeekOrigin.Begin);
Stream.Write((short)count);
}
}
public sealed class DisplayBoatHS : Packet
{
public DisplayBoatHS(Mobile beholder, BaseBoat boat) : base(0xF7)
{
var ents = boat.GetMovingEntities(true);
var ents = boat.GetMovingEntities(true);
EnsureCapacity(3 + 2 + 5 * 26);
EnsureCapacity(3 + 2 + 5 * 26);
Stream.Write((short)0); // count placeholder
Stream.Write((short)0); // count placeholder
var count = 0;
var count = 0;
foreach (var ent in ents)
foreach (var ent in ents)
{
if (!beholder.CanSee(ent))
{
if (!beholder.CanSee(ent))
{
continue;
}
// Embedded WorldItemHS packets
Stream.Write((byte)0xF3);
Stream.Write((short)0x1);
if (ent is BaseMulti bm)
{
Stream.Write((byte)0x02);
Stream.Write(bm.Serial);
// TODO: Mask no longer needed, merge with Item case?
Stream.Write((ushort)(bm.ItemID & 0x3FFF));
Stream.Write((byte)0);
Stream.Write((short)bm.Amount);
Stream.Write((short)bm.Amount);
Stream.Write((short)(bm.X & 0x7FFF));
Stream.Write((short)(bm.Y & 0x3FFF));
Stream.Write((sbyte)bm.Z);
Stream.Write((byte)bm.Light);
Stream.Write((short)bm.Hue);
Stream.Write((byte)bm.GetPacketFlags());
}
else if (ent is Mobile m)
{
Stream.Write((byte)0x01);
Stream.Write(m.Serial);
Stream.Write((short)m.Body);
Stream.Write((byte)0);
Stream.Write((short)1);
Stream.Write((short)1);
Stream.Write((short)(m.X & 0x7FFF));
Stream.Write((short)(m.Y & 0x3FFF));
Stream.Write((sbyte)m.Z);
Stream.Write((byte)m.Direction);
Stream.Write((short)m.Hue);
Stream.Write((byte)m.GetPacketFlags(true));
}
else if (ent is Item item)
{
Stream.Write((byte)0x00);
Stream.Write(item.Serial);
Stream.Write((ushort)(item.ItemID & 0xFFFF));
Stream.Write((byte)0);
Stream.Write((short)item.Amount);
Stream.Write((short)item.Amount);
Stream.Write((short)(item.X & 0x7FFF));
Stream.Write((short)(item.Y & 0x3FFF));
Stream.Write((sbyte)item.Z);
Stream.Write((byte)item.Light);
Stream.Write((short)item.Hue);
Stream.Write((byte)item.GetPacketFlags());
}
Stream.Write((short)0x00);
++count;
continue;
}
Stream.Seek(3, SeekOrigin.Begin);
Stream.Write((short)count);
// Embedded WorldItemHS packets
Stream.Write((byte)0xF3);
Stream.Write((short)0x1);
if (ent is BaseMulti bm)
{
Stream.Write((byte)0x02);
Stream.Write(bm.Serial);
// TODO: Mask no longer needed, merge with Item case?
Stream.Write((ushort)(bm.ItemID & 0x3FFF));
Stream.Write((byte)0);
Stream.Write((short)bm.Amount);
Stream.Write((short)bm.Amount);
Stream.Write((short)(bm.X & 0x7FFF));
Stream.Write((short)(bm.Y & 0x3FFF));
Stream.Write((sbyte)bm.Z);
Stream.Write((byte)bm.Light);
Stream.Write((short)bm.Hue);
Stream.Write((byte)bm.GetPacketFlags());
}
else if (ent is Mobile m)
{
Stream.Write((byte)0x01);
Stream.Write(m.Serial);
Stream.Write((short)m.Body);
Stream.Write((byte)0);
Stream.Write((short)1);
Stream.Write((short)1);
Stream.Write((short)(m.X & 0x7FFF));
Stream.Write((short)(m.Y & 0x3FFF));
Stream.Write((sbyte)m.Z);
Stream.Write((byte)m.Direction);
Stream.Write((short)m.Hue);
Stream.Write((byte)m.GetPacketFlags(true));
}
else if (ent is Item item)
{
Stream.Write((byte)0x00);
Stream.Write(item.Serial);
Stream.Write((ushort)(item.ItemID & 0xFFFF));
Stream.Write((byte)0);
Stream.Write((short)item.Amount);
Stream.Write((short)item.Amount);
Stream.Write((short)(item.X & 0x7FFF));
Stream.Write((short)(item.Y & 0x3FFF));
Stream.Write((sbyte)item.Z);
Stream.Write((byte)item.Light);
Stream.Write((short)item.Hue);
Stream.Write((byte)item.GetPacketFlags());
}
Stream.Write((short)0x00);
++count;
}
}
Stream.Seek(3, SeekOrigin.Begin);
Stream.Write((short)count);
}
}

View file

@ -14,66 +14,66 @@ public class HousePacketTests
[InlineData(0x1001u)]
public void TestBeginHouseCustomization(uint serial)
{
var expected = new BeginHouseCustomization((Serial)serial).Compile();
var expected = new BeginHouseCustomization((Serial)serial).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendBeginHouseCustomization((Serial)serial);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendBeginHouseCustomization((Serial)serial);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Theory]
[InlineData(0x1001u)]
public void TestEndHouseCustomization(uint serial)
{
var expected = new EndHouseCustomization((Serial)serial).Compile();
var expected = new EndHouseCustomization((Serial)serial).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendEndHouseCustomization((Serial)serial);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendEndHouseCustomization((Serial)serial);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Theory]
[InlineData(0x1001u, 0)]
[InlineData(0x1001u, 100)]
public void TestDesignStateGeneral(uint serial, int revision)
{
var expected = new DesignStateGeneral((Serial)serial, revision).Compile();
var expected = new DesignStateGeneral((Serial)serial, revision).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendDesignStateGeneral((Serial)serial, revision);
var ns = PacketTestUtilities.CreateTestNetState();
ns.SendDesignStateGeneral((Serial)serial, revision);
var result = ns.SendPipe.Reader.AvailableToRead();
AssertThat.Equal(result, expected);
}
}
[Fact]
public void TestHouseDesignStateDetailed()
{
Serial serial = (Serial)0x40000001;
var revision = 10;
var tiles = new MultiTileEntry[250];
for (var i = 0; i < tiles.Length; i++)
{
tiles[i] = new MultiTileEntry(
(ushort)i,
(byte)i,
(byte)i,
(byte)(i / 50),
TileFlag.None
);
}
var mcl = new MultiComponentList(tiles.ToList());
var expected = new DesignStateDetailed(
serial, revision, mcl.Min.X, mcl.Min.Y, mcl.Max.X, mcl.Max.Y, tiles
).Compile();
var actual = HousePackets.CreateHouseDesignStateDetailed(serial, revision, mcl);
AssertThat.Equal(actual, expected);
Serial serial = (Serial)0x40000001;
var revision = 10;
var tiles = new MultiTileEntry[250];
for (var i = 0; i < tiles.Length; i++)
{
tiles[i] = new MultiTileEntry(
(ushort)i,
(byte)i,
(byte)i,
(byte)(i / 50),
TileFlag.None
);
}
}
var mcl = new MultiComponentList(tiles.ToList());
var expected = new DesignStateDetailed(
serial, revision, mcl.Min.X, mcl.Min.Y, mcl.Max.X, mcl.Max.Y, tiles
).Compile();
var actual = HousePackets.CreateHouseDesignStateDetailed(serial, revision, mcl);
AssertThat.Equal(actual, expected);
}
}

Some files were not shown because too many files have changed in this diff Show more