Merge branch 'main' into Refactor/MagerySpell.cs
This commit is contained in:
commit
26b38ef924
284 changed files with 6151 additions and 4163 deletions
|
|
@ -3,7 +3,7 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"modernuoschemagenerator": {
|
||||
"version": "2.12.18",
|
||||
"version": "2.12.20",
|
||||
"commands": [
|
||||
"ModernUOSchemaGenerator"
|
||||
]
|
||||
|
|
|
|||
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
2
.github/workflows/code_quality.yml
vendored
2
.github/workflows/code_quality.yml
vendored
|
|
@ -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.
BIN
Distribution/Data/Items/ItemBounds.bin
Normal file
BIN
Distribution/Data/Items/ItemBounds.bin
Normal file
Binary file not shown.
|
|
@ -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}"
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
using Xunit;
|
||||
|
||||
namespace Server.Tests
|
||||
{
|
||||
[CollectionDefinition("Sequential Tests", DisableParallelization = true)]
|
||||
public class SequentialTestCollectionDefinition
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using Xunit;
|
|||
|
||||
namespace Server.Tests.Tests.Buffers;
|
||||
|
||||
[Collection("Sequential Tests")]
|
||||
[Collection("Sequential Server Tests")]
|
||||
public class STArrayPoolTests
|
||||
{
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using Xunit;
|
|||
|
||||
namespace Server.Tests.Buffers;
|
||||
|
||||
[Collection("Sequential Tests")]
|
||||
[Collection("Sequential Server Tests")]
|
||||
public class ValueStringBuilderTests
|
||||
{
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ using Xunit;
|
|||
|
||||
namespace Server.Tests.Network;
|
||||
|
||||
[Collection("Sequential Tests")]
|
||||
[Collection("Sequential Server Tests")]
|
||||
public class ClientVersionTests
|
||||
{
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ using Xunit;
|
|||
|
||||
namespace Server.Tests;
|
||||
|
||||
[Collection("Sequential Tests")]
|
||||
[Collection("Sequential Server Tests")]
|
||||
public class LocalizationEntryTests
|
||||
{
|
||||
[Fact]
|
||||
|
|
|
|||
137
Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs
Normal file
137
Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using Xunit;
|
|||
|
||||
namespace Server.Tests;
|
||||
|
||||
[Collection("Sequential Tests")]
|
||||
[Collection("Sequential Server Tests")]
|
||||
public class TestStringHelpers
|
||||
{
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
177
Projects/Server/EntityFinalizationTracker.cs
Normal file
177
Projects/Server/EntityFinalizationTracker.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
197
Projects/Server/Network/IPRateLimiter.cs
Normal file
197
Projects/Server/Network/IPRateLimiter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ public static class OutgoingContainerPackets
|
|||
return;
|
||||
}
|
||||
|
||||
if (ns.NewSpellbook)
|
||||
if (Core.AOS && ns.NewSpellbook)
|
||||
{
|
||||
ns.SendNewSpellbookContent(book, graphic, offset, content);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using Xunit;
|
|||
|
||||
namespace Server.Tests.Gumps;
|
||||
|
||||
[Collection("Sequential Tests")]
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class TestLayoutGumps
|
||||
{
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
137
Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
Normal file
137
Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue