diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 73eb8d9a0..08b49e256 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "modernuoschemagenerator": { - "version": "2.12.18", + "version": "2.12.20", "commands": [ "ModernUOSchemaGenerator" ] diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index e4c8acc17..c2f1c3b2f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -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: diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index ae9f3e078..e7bbc2ebe 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -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 }} diff --git a/Distribution/Data/Binary/Bounds.bin b/Distribution/Data/Binary/Bounds.bin deleted file mode 100644 index 11431362e..000000000 Binary files a/Distribution/Data/Binary/Bounds.bin and /dev/null differ diff --git a/Distribution/Data/Items/ItemBounds.bin b/Distribution/Data/Items/ItemBounds.bin new file mode 100644 index 000000000..1e0214cbf Binary files /dev/null and b/Distribution/Data/Items/ItemBounds.bin differ diff --git a/Projects/Logger/LogFactory.cs b/Projects/Logger/LogFactory.cs index bed3ea20e..d6bc9a41c 100644 --- a/Projects/Logger/LogFactory.cs +++ b/Projects/Logger/LogFactory.cs @@ -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} {NewLine}{Exception}" )) diff --git a/Projects/Server.Tests/Fixtures/ServerFixture.cs b/Projects/Server.Tests/Fixtures/ServerFixture.cs index b57248286..f66a578db 100644 --- a/Projects/Server.Tests/Fixtures/ServerFixture.cs +++ b/Projects/Server.Tests/Fixtures/ServerFixture.cs @@ -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, IDisposable { - // Global setup - static ServerFixture() + public ServerFixture() { Core.ApplicationAssembly = Assembly.GetExecutingAssembly(); // Server.Tests.dll diff --git a/Projects/Server.Tests/Fixtures/TestMapDefinitions.cs b/Projects/Server.Tests/Fixtures/TestMapDefinitions.cs index a94ae799a..e6e05dc72 100644 --- a/Projects/Server.Tests/Fixtures/TestMapDefinitions.cs +++ b/Projects/Server.Tests/Fixtures/TestMapDefinitions.cs @@ -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); } } diff --git a/Projects/Server.Tests/Helpers/GumpUtilities.cs b/Projects/Server.Tests/Helpers/GumpUtilities.cs index 94ebedca2..f5a910db8 100644 --- a/Projects/Server.Tests/Helpers/GumpUtilities.cs +++ b/Projects/Server.Tests/Helpers/GumpUtilities.cs @@ -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 strings, string value) diff --git a/Projects/Server.Tests/SequentialTestCollectionDefinition.cs b/Projects/Server.Tests/SequentialTestCollectionDefinition.cs deleted file mode 100644 index 2db7f513c..000000000 --- a/Projects/Server.Tests/SequentialTestCollectionDefinition.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Xunit; - -namespace Server.Tests -{ - [CollectionDefinition("Sequential Tests", DisableParallelization = true)] - public class SequentialTestCollectionDefinition - { - } -} diff --git a/Projects/Server.Tests/Server.Tests.csproj b/Projects/Server.Tests/Server.Tests.csproj index 9733e59fa..8da175d26 100644 --- a/Projects/Server.Tests/Server.Tests.csproj +++ b/Projects/Server.Tests/Server.Tests.csproj @@ -5,9 +5,9 @@ Server.Tests - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Projects/Server.Tests/Tests/Buffers/STArrayPoolTests.cs b/Projects/Server.Tests/Tests/Buffers/STArrayPoolTests.cs index 3a51999c0..cf3b54871 100644 --- a/Projects/Server.Tests/Tests/Buffers/STArrayPoolTests.cs +++ b/Projects/Server.Tests/Tests/Buffers/STArrayPoolTests.cs @@ -4,7 +4,7 @@ using Xunit; namespace Server.Tests.Tests.Buffers; -[Collection("Sequential Tests")] +[Collection("Sequential Server Tests")] public class STArrayPoolTests { [Theory] diff --git a/Projects/Server.Tests/Tests/Buffers/ValueStringBuilderTests.cs b/Projects/Server.Tests/Tests/Buffers/ValueStringBuilderTests.cs index 52a892772..0cc89d821 100644 --- a/Projects/Server.Tests/Tests/Buffers/ValueStringBuilderTests.cs +++ b/Projects/Server.Tests/Tests/Buffers/ValueStringBuilderTests.cs @@ -3,7 +3,7 @@ using Xunit; namespace Server.Tests.Buffers; -[Collection("Sequential Tests")] +[Collection("Sequential Server Tests")] public class ValueStringBuilderTests { [Theory] diff --git a/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs b/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs index 6e0f3f039..471a5584c 100644 --- a/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs +++ b/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs @@ -2,7 +2,7 @@ using Xunit; namespace Server.Tests.Network; -[Collection("Sequential Tests")] +[Collection("Sequential Server Tests")] public class ClientVersionTests { [Theory] diff --git a/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs b/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs index 1425e7b24..f6611a67b 100644 --- a/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs +++ b/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs @@ -3,7 +3,8 @@ using Xunit; namespace Server.Tests; -public sealed class WorldLocationTests : IClassFixture +[Collection("Sequential Server Tests")] +public sealed class WorldLocationTests { private static Map CreateMap(string name) => new(0, 0, 0, 1, 1, 0, name, MapRules.Internal); diff --git a/Projects/Server.Tests/Tests/Items/ContainerTests.cs b/Projects/Server.Tests/Tests/Items/ContainerTests.cs index d6d597754..0b258a717 100644 --- a/Projects/Server.Tests/Tests/Items/ContainerTests.cs +++ b/Projects/Server.Tests/Tests/Items/ContainerTests.cs @@ -5,7 +5,8 @@ using Xunit; namespace Server.Tests; -public class ContainerTests : IClassFixture +[Collection("Sequential Server Tests")] +public class ContainerTests { [Fact] public void TestFindItemsByType() diff --git a/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs b/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs index f10c21555..154cfe7a6 100644 --- a/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs +++ b/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs @@ -2,7 +2,7 @@ using Xunit; namespace Server.Tests; -[Collection("Sequential Tests")] +[Collection("Sequential Server Tests")] public class LocalizationEntryTests { [Fact] diff --git a/Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs b/Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs new file mode 100644 index 000000000..af0f877e2 --- /dev/null +++ b/Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs @@ -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)); + } +} diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs index a384a2d44..e69657312 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs @@ -7,7 +7,8 @@ using Xunit; namespace Server.Tests.Network; -public class AccountPacketTests : IClassFixture +[Collection("Sequential Server Tests")] +public class AccountPacketTests { private class MockedAccount : IAccount { diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs index 1730017b9..0b3435481 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ContainerPacketTests.cs @@ -4,8 +4,8 @@ using Xunit; namespace Server.Tests.Network { - [Collection("Sequential Tests")] - public class ContainerPacketTests : IClassFixture + [Collection("Sequential Server Tests")] + public class ContainerPacketTests { [Fact] diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs index e5ec188a6..6caf6f3ee 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/DamagePacketTests.cs @@ -3,7 +3,8 @@ using Xunit; namespace Server.Tests.Network { - public class DamagePacketTests : IClassFixture + [Collection("Sequential Server Tests")] +public class DamagePacketTests { [Theory, InlineData(10), InlineData(-5), InlineData(1024)] public void TestDamagePacketOld(int inputAmount) diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs index 6192c90a6..35f139d24 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/EquipmentPacketTests.cs @@ -4,7 +4,8 @@ using Xunit; namespace Server.Tests.Network { - public class EquipmentPacketTests : IClassFixture + [Collection("Sequential Server Tests")] +public class EquipmentPacketTests { [Theory] [InlineData(null, false)] diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs index 4247ca123..a93d23dfe 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs @@ -3,8 +3,8 @@ using Xunit; namespace Server.Tests.Network; -[Collection("Sequential Tests")] -public class GumpPacketTests : IClassFixture +[Collection("Sequential Server Tests")] +public class GumpPacketTests { [Theory] [InlineData(100, 10)] diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs index 48bbb0c32..4e2d2d50f 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/ItemPacketTests.cs @@ -3,7 +3,8 @@ using Xunit; namespace Server.Tests.Network { - public class ItemPacketTests : IClassFixture + [Collection("Sequential Server Tests")] +public class ItemPacketTests { [Fact] public void TestWorldItemPacket() diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs index 28872e9e6..09344d451 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MapPacketTests.cs @@ -3,7 +3,8 @@ using Xunit; namespace Server.Tests.Network { - public class MapPatchesTests : IClassFixture + [Collection("Sequential Server Tests")] +public class MapPatchesTests { [Fact] public void TestMapPatches() diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs index 243bff188..0304658ad 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MenuPacketTests.cs @@ -27,8 +27,8 @@ namespace Server.Tests.Network } } - [Collection("Sequential Tests")] - public class MenuPacketTests : IClassFixture + [Collection("Sequential Server Tests")] + public class MenuPacketTests { [Fact] public void TestDisplayItemListMenu() diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs index 348a06253..f0e41c4b3 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePacketTests.cs @@ -3,8 +3,8 @@ using Xunit; namespace Server.Tests.Network; -[Collection("Sequential Tests")] -public class MobilePacketTests : IClassFixture +[Collection("Sequential Server Tests")] +public class MobilePacketTests { [Fact] public void TestDeathAnimation() diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs index e438fab9e..a5430c8ac 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs @@ -3,7 +3,8 @@ using Xunit; namespace Server.Tests.Network { - public class MovementPacketTests : IClassFixture + [Collection("Sequential Server Tests")] +public class MovementPacketTests { [Theory] [InlineData(0)] diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs index 2e20a6f80..bb4822761 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/PlayerPacketTests.cs @@ -5,8 +5,8 @@ using Xunit; namespace Server.Tests.Network { - [Collection("Sequential Tests")] - public class PlayerPacketTests : IClassFixture + [Collection("Sequential Server Tests")] + public class PlayerPacketTests { [Theory] [InlineData(StatLockType.Down, StatLockType.Up, StatLockType.Locked)] diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs index 2519f5f7c..2c79b421b 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/SecureTradePacketTests.cs @@ -4,7 +4,8 @@ using Xunit; namespace Server.Tests.Network { - public class SecureTradePacketTests : IClassFixture + [Collection("Sequential Server Tests")] +public class SecureTradePacketTests { [Theory] [InlineData("short-name")] diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs index 0a353169d..5298e728e 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorBuyPacketTests.cs @@ -5,8 +5,8 @@ using Xunit; namespace Server.Tests.Network { - [Collection("Sequential Tests")] - public class VendorBuyPacketTests : IClassFixture + [Collection("Sequential Server Tests")] + public class VendorBuyPacketTests { [Theory] [InlineData(ProtocolChanges.None)] diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs index 77685d9a9..e19b8bb00 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VendorSellPacketTests.cs @@ -5,8 +5,8 @@ using Xunit; namespace Server.Tests.Network { - [Collection("Sequential Tests")] - public class VendorSellPacketTests : IClassFixture + [Collection("Sequential Server Tests")] + public class VendorSellPacketTests { [Fact] public void TestVendorSellList() diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs index 13a6ac9c7..d65b7219b 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs @@ -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 + [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); } } diff --git a/Projects/Server.Tests/Tests/Serialization/TypeConverterTests.cs b/Projects/Server.Tests/Tests/Serialization/TypeConverterTests.cs index a0ec96034..ead74f63b 100644 --- a/Projects/Server.Tests/Tests/Serialization/TypeConverterTests.cs +++ b/Projects/Server.Tests/Tests/Serialization/TypeConverterTests.cs @@ -8,7 +8,8 @@ using Xunit; namespace Server.Tests; -public class TypeConverterTests : IClassFixture +[Collection("Sequential Server Tests")] +public class TypeConverterTests { [Fact] public void TestReadAfterWrite() diff --git a/Projects/Server.Tests/Tests/Timer/TimerTests.cs b/Projects/Server.Tests/Tests/Timer/TimerTests.cs index 6f00b6684..15a6f73d8 100644 --- a/Projects/Server.Tests/Tests/Timer/TimerTests.cs +++ b/Projects/Server.Tests/Tests/Timer/TimerTests.cs @@ -3,8 +3,8 @@ using Xunit; namespace Server.Tests; -[Collection("Sequential Tests")] -public class TimerTests : IClassFixture +[Collection("Sequential Server Tests")] +public class TimerTests { [Theory] [InlineData(0L, 8L)] diff --git a/Projects/Server.Tests/Tests/Utility/StringHelperTests.cs b/Projects/Server.Tests/Tests/Utility/StringHelperTests.cs index 3c34488b0..f428a060a 100644 --- a/Projects/Server.Tests/Tests/Utility/StringHelperTests.cs +++ b/Projects/Server.Tests/Tests/Utility/StringHelperTests.cs @@ -4,7 +4,7 @@ using Xunit; namespace Server.Tests; -[Collection("Sequential Tests")] +[Collection("Sequential Server Tests")] public class TestStringHelpers { [Theory] diff --git a/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs b/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs index a8d0241e4..6eefe5a8b 100644 --- a/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs +++ b/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs @@ -2,8 +2,8 @@ using Xunit; namespace Server.Tests; -[Collection("Sequential Tests")] -public class VirtualSerialTests : IClassFixture +[Collection("Sequential Server Tests")] +public class VirtualSerialTests { [Fact] public void TestNewVirtualGetsAndRollover() diff --git a/Projects/Server/AssemblyHandler.cs b/Projects/Server/AssemblyHandler.cs index 2ff44bd4f..4e3f45e8b 100644 --- a/Projects/Server/AssemblyHandler.cs +++ b/Projects/Server/AssemblyHandler.cs @@ -234,10 +234,10 @@ public class TypeCache private static ILogger logger = LogFactory.GetLogger(typeof(TypeCache)); #endif - private Dictionary _nameMap = new(); - private Dictionary _nameMapInsensitive = new(); - private Dictionary _fullNameMap = new(); - private Dictionary _fullNameMapInsensitive = new(); + private readonly Dictionary _nameMap = []; + private readonly Dictionary _nameMapInsensitive = []; + private readonly Dictionary _fullNameMap = []; + private readonly Dictionary _fullNameMapInsensitive = []; public TypeCache(Assembly asm) { @@ -248,15 +248,6 @@ public class TypeCache var fullNameMap = new Dictionary>(); var fullNameMapInsensitive = new Dictionary>(); - [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 }; + 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(); + _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(); + _values = map.TryGetValue(hash, out var values) ? values : []; } _index = 0; diff --git a/Projects/Server/Attributes.cs b/Projects/Server/Attributes.cs index daab18e7e..ffdad7c59 100644 --- a/Projects/Server/Attributes.cs +++ b/Projects/Server/Attributes.cs @@ -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; } diff --git a/Projects/Server/Buffers/ValueStringBuilder.cs b/Projects/Server/Buffers/ValueStringBuilder.cs index 3196ca45e..b6d4ab2f1 100644 --- a/Projects/Server/Buffers/ValueStringBuilder.cs +++ b/Projects/Server/Buffers/ValueStringBuilder.cs @@ -309,7 +309,7 @@ public ref struct ValueStringBuilder } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Append(ReadOnlySpan value) + public void Append(scoped ReadOnlySpan value) { int pos = _length; if (pos > _chars.Length - value.Length) diff --git a/Projects/Server/Client/ArtData.cs b/Projects/Server/Client/ArtData.cs index ba6e4167a..d8f54d868 100644 --- a/Projects/Server/Client/ArtData.cs +++ b/Projects/Server/Client/ArtData.cs @@ -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 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 LoadMulRanges(string idxPath) diff --git a/Projects/Server/Client/ClientVersion.cs b/Projects/Server/Client/ClientVersion.cs index 008650aae..37cad2f5c 100644 --- a/Projects/Server/Client/ClientVersion.cs +++ b/Projects/Server/Client/ClientVersion.cs @@ -30,6 +30,7 @@ public class ClientVersion : IComparable, IComparer. * + *************************************************************************/ + +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 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 ")] + [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 "); + 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 _entities = []; + private static readonly HashSet _finalizedHashes = []; + + public static void TrackEntity(T entity) where T : IEntity + { + lock (_sync) + { + var hash = RuntimeHelpers.GetHashCode(entity); + _entities.Add(new TrackedEntity(new WeakReference(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; +} diff --git a/Projects/Server/EventLoopTasks.cs b/Projects/Server/EventLoopTasks.cs index d8d587156..aeb3351e0 100644 --- a/Projects/Server/EventLoopTasks.cs +++ b/Projects/Server/EventLoopTasks.cs @@ -21,18 +21,29 @@ namespace Server; public sealed class EventLoopContext : SynchronizationContext { - private readonly ConcurrentQueue _queue; - private readonly Thread _mainThread; - - public EventLoopContext() + public enum Priority { - _queue = new ConcurrentQueue(); + Normal, + High + } + + private readonly ConcurrentQueue _queue; + private readonly ConcurrentQueue _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++) { diff --git a/Projects/Server/GarbageCollection/Gen2GcCallback.cs b/Projects/Server/GarbageCollection/Gen2GcCallback.cs index fc23db8ce..97b4b91ff 100644 --- a/Projects/Server/GarbageCollection/Gen2GcCallback.cs +++ b/Projects/Server/GarbageCollection/Gen2GcCallback.cs @@ -13,18 +13,31 @@ namespace System; /// internal sealed class Gen2GcCallback : CriticalFinalizerObject { - private readonly Func _callback; + private readonly Func? _callback0; + private readonly Func? _callback1; private GCHandle _weakTargetObj; + private Gen2GcCallback(Func callback) => _callback0 = callback; + private Gen2GcCallback(Func callback, object targetObj) { - _callback = callback; + _callback1 = callback; _weakTargetObj = GCHandle.Alloc(targetObj, GCHandleType.Weak); } /// /// 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. + /// + public static void Register(Func callback) + { + // Create an unreachable object that remembers the callback function and target object. + new Gen2GcCallback(callback); + } + + /// + /// 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 } } diff --git a/Projects/Server/Items/Container.cs b/Projects/Server/Items/Container.cs index 2d50aa8b3..bf4fab0b1 100644 --- a/Projects/Server/Items/Container.cs +++ b/Projects/Server/Items/Container.cs @@ -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 droppedItems) { var dropItems = new List(); var stackItems = new List(); diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index 2a17627ff..9726e1d34 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -328,7 +328,7 @@ public class Item : IHued, IComparable, 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, 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, ISpawnable, IObjectPropertyListEnt } } +#if TRACK_LEAKS + ~Item() + { + EntityFinalizationTracker.NotifyFinalized(this); + } +#endif + public virtual void OnDelete() { if (Spawner != null) diff --git a/Projects/Server/Items/ItemBounds.cs b/Projects/Server/Items/ItemBounds.cs index 4b957b80e..5f94eeca8 100644 --- a/Projects/Server/Items/ItemBounds.cs +++ b/Projects/Server/Items/ItemBounds.cs @@ -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; 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); } } } diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index d25827f6a..b1a76f014 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -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) { diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 66064b87b..5b4e7a992 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -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, 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, ISpawnable, IObjectPro } } +#if TRACK_LEAKS + ~Mobile() + { + EntityFinalizationTracker.NotifyFinalized(this); + } +#endif + /// /// Overridable. Virtual event invoked before the Mobile is deleted. /// @@ -5371,13 +5378,32 @@ public partial class Mobile : IHued, IComparable, 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 ghostChars = (GhostChars ?? DefaultGhostChars).AsSpan(); + + var length = text.Length; + char[] rentedChars = null; + Span chars = length <= 256 + ? stackalloc char[length] + : rentedChars = STArrayPool.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.Shared.Return(rentedChars); + } } - text = sb.ToString(); context = m_GhostMutateContext; return true; } @@ -5418,7 +5444,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro public virtual bool CheckHearsMutatedSpeech(Mobile m, object context) => context != m_GhostMutateContext || m.Alive && !m.CanHearGhosts; - private void AddSpeechItemsFrom(List list, Container cont) + private static void AddSpeechItemsFrom(List list, Container cont) { for (var i = 0; i < cont.Items.Count; ++i) { @@ -5470,33 +5496,12 @@ public partial class Mobile : IHued, IComparable, 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, 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); diff --git a/Projects/Server/Network/Firewall/Firewall.cs b/Projects/Server/Network/Firewall/Firewall.cs index e80050416..3321b5f46 100644 --- a/Projects/Server/Network/Firewall/Firewall.cs +++ b/Projects/Server/Network/Firewall/Firewall.cs @@ -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 _isBlockedCache = new(); + private static readonly ConcurrentDictionary _isBlockedCache = []; + private static readonly ReaderWriterLockSlim _firewallLock = new(LockRecursionPolicy.NoRecursion); - private static readonly SortedSet _firewallSet = new(); + private static int _firewallVersion; + private static readonly SortedSet _firewallSet = []; - public static SortedSet FirewallSet => _firewallSet; + public static int FirewallSetCount => _firewallSet.Count; + + public static void ReadFirewallSet(Action> 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 diff --git a/Projects/Server/Network/Firewall/IFirewallEntry.cs b/Projects/Server/Network/Firewall/IFirewallEntry.cs index 4a68f2441..8a09f7121 100644 --- a/Projects/Server/Network/Firewall/IFirewallEntry.cs +++ b/Projects/Server/Network/Firewall/IFirewallEntry.cs @@ -40,12 +40,12 @@ public interface IFirewallEntry : IComparable return 1; } - if (MaxIpAddress < other.MaxIpAddress) + if (MaxIpAddress > other.MaxIpAddress) { return -1; } - if (MaxIpAddress > other.MaxIpAddress) + if (MaxIpAddress < other.MaxIpAddress) { return 1; } diff --git a/Projects/Server/Network/IPLimiter.cs b/Projects/Server/Network/IPLimiter.cs deleted file mode 100644 index dc4717645..000000000 --- a/Projects/Server/Network/IPLimiter.cs +++ /dev/null @@ -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 . * - *************************************************************************/ - -using System; -using System.Collections.Generic; -using System.Net; - -namespace Server.Misc; - -public static class IPLimiter -{ - private static readonly SortedSet _connectionAttempts = []; - private static readonly SortedSet _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 - { - 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); - } -} diff --git a/Projects/Server/Network/IPRateLimiter.cs b/Projects/Server/Network/IPRateLimiter.cs new file mode 100644 index 000000000..5de0ad552 --- /dev/null +++ b/Projects/Server/Network/IPRateLimiter.cs @@ -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 . * + *************************************************************************/ + +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 _statsPool = []; + private const int MaxPoolSize = 32_768; + + private readonly SemaphoreSlim _cleanupSignal = new(0, 1); + private readonly ConcurrentDictionary _ipAttempts; + private readonly ConcurrentQueue _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; + } + } +} diff --git a/Projects/Server/Network/Packets/OutgoingContainerPackets.cs b/Projects/Server/Network/Packets/OutgoingContainerPackets.cs index 1f72e395c..af36d54f3 100644 --- a/Projects/Server/Network/Packets/OutgoingContainerPackets.cs +++ b/Projects/Server/Network/Packets/OutgoingContainerPackets.cs @@ -33,7 +33,7 @@ public static class OutgoingContainerPackets return; } - if (ns.NewSpellbook) + if (Core.AOS && ns.NewSpellbook) { ns.SendNewSpellbookContent(book, graphic, offset, content); } diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index d2c6751c8..5e5fe3a1b 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -13,15 +13,17 @@ * along with this program. If not, see . * *************************************************************************/ +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 listeningAddresses = []; List 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(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); } } } diff --git a/Projects/Server/Regions/Region.cs b/Projects/Server/Regions/Region.cs index 0045a1ffa..a53e6466f 100644 --- a/Projects/Server/Regions/Region.cs +++ b/Projects/Server/Regions/Region.cs @@ -127,7 +127,7 @@ public class Region : IComparable, IValueLinkListNode 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 area) : this( name, map, priority, @@ -147,7 +147,7 @@ public class Region : IComparable, IValueLinkListNode 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 area) : this( name, map, parent, @@ -314,7 +314,7 @@ public class Region : IComparable, IValueLinkListNode 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 rects) { var ret = new Rectangle3D[rects.Length]; diff --git a/Projects/Server/Serialization/BinaryFileReader.cs b/Projects/Server/Serialization/BinaryFileReader.cs index f540f8e2e..d9b5c0dc1 100644 --- a/Projects/Server/Serialization/BinaryFileReader.cs +++ b/Projects/Server/Serialization/BinaryFileReader.cs @@ -167,7 +167,7 @@ public sealed unsafe class BinaryFileReader : IDisposable, IGenericReader public Serial ReadSerial() => _reader.ReadSerial(); /// - /// 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. ///
If the byte returns 1 => and translate into a Type via the
///
If the byte returns 2 =>
///
else return null
diff --git a/Projects/Server/Serialization/GenericEntityPersistence.cs b/Projects/Server/Serialization/GenericEntityPersistence.cs index e9ef932c1..12e929711 100644 --- a/Projects/Server/Serialization/GenericEntityPersistence.cs +++ b/Projects/Server/Serialization/GenericEntityPersistence.cs @@ -176,12 +176,12 @@ public class GenericEntityPersistence : GenericPersistence, IGenericEntityPer /** * Legacy ReadTypes for backward compatibility with old saves that still have a tdb file */ - private unsafe Dictionary ReadTypes(string savePath) + private unsafe Dictionary 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 : GenericPersistence, IGenericEntityPer var dataReader = new UnmanagedDataReader(ptr, accessor.Length); var count = dataReader.ReadInt(); - var types = new Dictionary(count); + var types = new Dictionary(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 : GenericPersistence, IGenericEntityPer var version = dataReader.ReadInt(); - Dictionary 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 : 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 : 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(); diff --git a/Projects/Server/Serialization/IGenericReader.cs b/Projects/Server/Serialization/IGenericReader.cs index e3fe07deb..4e59e4e98 100644 --- a/Projects/Server/Serialization/IGenericReader.cs +++ b/Projects/Server/Serialization/IGenericReader.cs @@ -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.Shared.Rent(byteLength); + try + { + Read(buffer.AsSpan(0, byteLength)); + return new BitArray(buffer) { Length = bitLength }; + } + finally + { + ArrayPool.Shared.Return(buffer); + } } TextDefinition ReadTextDefinition() diff --git a/Projects/Server/Serialization/IGenericWriter.cs b/Projects/Server/Serialization/IGenericWriter.cs index 63ac04970..72a8728d0 100644 --- a/Projects/Server/Serialization/IGenericWriter.cs +++ b/Projects/Server/Serialization/IGenericWriter.cs @@ -163,14 +163,21 @@ public interface IGenericWriter public void Write(BitArray bitArray) { - var bytesLength = (bitArray.Length - 1 + (1 << 3)) >>> 3; - var arrayBuffer = ArrayPool.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.Shared.Return(arrayBuffer); + var arrayBuffer = ArrayPool.Shared.Rent(byteLength); + try + { + bitArray.CopyTo(arrayBuffer, 0); + Write(arrayBuffer.AsSpan(0, byteLength)); + } + finally + { + ArrayPool.Shared.Return(arrayBuffer); + } } void Write(TextDefinition def) diff --git a/Projects/Server/Serialization/UnmanagedDataReader.cs b/Projects/Server/Serialization/UnmanagedDataReader.cs index 9babb86fd..f5a6967be 100644 --- a/Projects/Server/Serialization/UnmanagedDataReader.cs +++ b/Projects/Server/Serialization/UnmanagedDataReader.cs @@ -218,7 +218,7 @@ public unsafe class UnmanagedDataReader : IGenericReader public Serial ReadSerial() => (Serial)ReadUInt(); /// - /// 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. ///
If the byte returns 1 => and translate into a Type via the
///
If the byte returns 2 =>
///
else return null
diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index 9689bfd32..9142059c6 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -37,12 +37,18 @@ - + - + + + <_Parameter1>Server.Tests + + + <_Parameter1>UOContent.Tests + diff --git a/Projects/Server/Text/TextEncoding.cs b/Projects/Server/Text/TextEncoding.cs index bf8051d71..b2f290f78 100644 --- a/Projects/Server/Text/TextEncoding.cs +++ b/Projects/Server/Text/TextEncoding.cs @@ -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 span, Encoding encoding, bool safeString = false) { - string s = encoding.GetString(span); - if (!safeString) { - return s; + return encoding.GetString(span); } - ReadOnlySpan chars = s.AsSpan(); + var charCount = encoding.GetMaxCharCount(span.Length); - using var sb = new ValueStringBuilder(stackalloc char[256]); - var hasDoneAnyReplacements = false; + char[] rentedChars = null; + Span chars = charCount <= 256 + ? stackalloc char[charCount] + : rentedChars = STArrayPool.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.Shared.Return(rentedChars); } } - - return !hasDoneAnyReplacements ? s : sb.ToString(); } } diff --git a/Projects/Server/Utilities/Html.cs b/Projects/Server/Utilities/Html.cs index 00b8c91cd..91d2b6f3b 100644 --- a/Projects/Server/Utilities/Html.cs +++ b/Projects/Server/Utilities/Html.cs @@ -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) => $"{text}"; + public static RawInterpolatedStringHandler Color( + scoped ref RawInterpolatedStringHandler textHandler, + ReadOnlySpan 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 text, int color) => - $"{text}"; + public static RawInterpolatedStringHandler Color( + this ReadOnlySpan text, + ReadOnlySpan color, + int size = -1, + byte fontStyle = 0 + ) + { + if (color != Span.Empty) + { + if (size > -1) + { + if (fontStyle > 0) + { + return $"{text}"; + } + + return $"{text}"; + } + + if (fontStyle > 0) + { + return $"{text}"; + } + + return $"{text}"; + } + + if (size > -1) + { + if (fontStyle > 0) + { + return $"{text}"; + } + + return $"{text}"; + } + + if (fontStyle > 0) + { + return $"{text}"; + } + + return $"{text}"; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Color(this string text, int color, int size) => $"{text}"; + public static RawInterpolatedStringHandler Color( + this ReadOnlySpan text, + int color, + int size = -1, + byte fontStyle = 0 + ) + { + if (color > -1) + { + return text.Color($"#{color:X6}", size, fontStyle); + } + + return text.Color((ReadOnlySpan)default, size, fontStyle); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Color(this string text, string color) => $"{text}"; + 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) => - $"{text}"; + public static string Color( + this string text, + ReadOnlySpan color, + int size = -1, + byte fontStyle = 0 + ) + { + var textHandler = ((ReadOnlySpan)text).Color(color, size, fontStyle); + var str = textHandler.Text.ToString(); + textHandler.Clear(); + return str; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Center(this string text) => $"
{text}
"; + public static string Color( + this string text, + int color, + int size = -1, + byte fontStyle = 0 + ) + { + var textHandler = ((ReadOnlySpan)text).Color(color, size, fontStyle); + var str = textHandler.Text.ToString(); + textHandler.Clear(); + return str; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RawInterpolatedStringHandler Center(this ReadOnlySpan text) => $"
{text}
"; + public static string Center( + this string text, int color, int size = -1, byte fontStyle = 0 + ) + { + var handler = Center((ReadOnlySpan)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) => - $"
{text}
"; + public static string Center( + this string text, ReadOnlySpan color, int size = -1, byte fontStyle = 0 + ) + { + var handler = Center((ReadOnlySpan)text, color, size, fontStyle); + var str = handler.Text.ToString(); + handler.Clear(); + + return str; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RawInterpolatedStringHandler Center(this ReadOnlySpan text, int color) => - $"
{text}
"; + public static string Center(this string text) => text.Center(-1); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Center(this string text, int color, int size) => - $"
{text}
"; + public static RawInterpolatedStringHandler Center(this ReadOnlySpan text) => Center(text, -1); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Center(this string text, string color) => - $"
{text}
"; + public static RawInterpolatedStringHandler Center( + this ReadOnlySpan text, int color, int size = -1, byte fontStyle = 0 + ) => Color($"
{text}
", color, size, fontStyle); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Center(this string text, string color, int size) => - $"
{text}
"; + public static RawInterpolatedStringHandler Center( + this ReadOnlySpan text, ReadOnlySpan color, int size = -1, byte fontStyle = 0 + ) => Color($"
{text}
", color, size, fontStyle); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Right(this string text) => $"{text}"; + 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) => - $"{text}"; + public static string Right( + this string text, int color, int size = -1, byte fontStyle = 0 + ) + { + var handler = Right((ReadOnlySpan)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) => - $"{text}"; + public static string Right( + this string text, ReadOnlySpan color, int size = -1, byte fontStyle = 0 + ) + { + var handler = Right((ReadOnlySpan)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) => $"{text}"; + public static string Right(this string text) => text.Right(-1); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Right(this string text, string color, int size) => - $"{text}"; + 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 text, int color, int size = -1, byte fontStyle = 0 + ) => Color($"{text}", color, size, fontStyle); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RawInterpolatedStringHandler Right( + this ReadOnlySpan text, ReadOnlySpan color, int size = -1, byte fontStyle = 0 + ) => Color($"{text}", color, size, fontStyle); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RawInterpolatedStringHandler Right(this ReadOnlySpan text) => text.Right(-1); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string EscapeHtml(this string input) => diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 61545077d..6ef4aa72c 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -745,7 +745,7 @@ public static partial class Utility } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T RandomList(params T[] list) => list.RandomElement(); + public static T RandomList(params ReadOnlySpan list) => list.RandomElement(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T RandomElement(this ReadOnlySpan 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(this IList source, params IList[] arrays) => + public static T[] Combine(this IList source, params ReadOnlySpan> arrays) => source.Combine(false, arrays); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T[] CombinePooled(this IList source, params IList[] arrays) => + public static T[] CombinePooled(this IList source, params ReadOnlySpan> arrays) => source.Combine(true, arrays); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T[] Combine(this IList source, bool pooled, params IList[] arrays) + public static T[] Combine(this IList source, bool pooled, params ReadOnlySpan> 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); + } } diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index 14a5ba31f..a8ecb665d 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -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)] diff --git a/Projects/UOContent.Tests/Fixtures/ServerFixture.cs b/Projects/UOContent.Tests/Fixtures/UOContentFixture.cs similarity index 73% rename from Projects/UOContent.Tests/Fixtures/ServerFixture.cs rename to Projects/UOContent.Tests/Fixtures/UOContentFixture.cs index a78941054..658bbb9f9 100644 --- a/Projects/UOContent.Tests/Fixtures/ServerFixture.cs +++ b/Projects/UOContent.Tests/Fixtures/UOContentFixture.cs @@ -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, 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); } } diff --git a/Projects/UOContent.Tests/Tests/Accounting/AccountHandlerTests.cs b/Projects/UOContent.Tests/Tests/Accounting/AccountHandlerTests.cs new file mode 100644 index 000000000..429d252eb --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Accounting/AccountHandlerTests.cs @@ -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 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, + }; } -} \ No newline at end of file + 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")); + } +} diff --git a/Projects/UOContent.Tests/Tests/Engines/Chat/ChatPacketTests.cs b/Projects/UOContent.Tests/Tests/Engines/Chat/ChatPacketTests.cs index 7ecb91c62..6b2e4b391 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Chat/ChatPacketTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Chat/ChatPacketTests.cs @@ -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); - } -} \ No newline at end of file + } +} diff --git a/Projects/UOContent.Tests/Tests/Engines/Chat/Packets.cs b/Projects/UOContent.Tests/Tests/Engines/Chat/Packets.cs index 58c872652..5cd71285e 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Chat/Packets.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Chat/Packets.cs @@ -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); } -} \ No newline at end of file + else + { + Stream.Write(0); + } + + Stream.WriteBigUniNull(param1); + Stream.WriteBigUniNull(param2); + } +} diff --git a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs new file mode 100644 index 000000000..65da0271c --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs @@ -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(); + + // 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(); + } + } +} diff --git a/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs b/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs new file mode 100644 index 000000000..b08a49007 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs @@ -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); + } +} diff --git a/Projects/UOContent.Tests/Tests/Engines/Help/HelpTopic.cs b/Projects/UOContent.Tests/Tests/Engines/Help/HelpTopic.cs index 235fb5c69..58b76edee 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Help/HelpTopic.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Help/HelpTopic.cs @@ -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); - } -} \ No newline at end of file + Stream.Write((short)0x17); + Stream.Write((byte)1); + Stream.Write(topicID); + Stream.Write(display); + } +} diff --git a/Projects/UOContent.Tests/Tests/Engines/Help/TestHelpTopicPacket.cs b/Projects/UOContent.Tests/Tests/Engines/Help/TestHelpTopicPacket.cs index feca1c09a..0c6db04cc 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Help/TestHelpTopicPacket.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Help/TestHelpTopicPacket.cs @@ -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); - } -} \ No newline at end of file + } +} diff --git a/Projects/UOContent.Tests/Tests/Engines/ML Quests/Packets.cs b/Projects/UOContent.Tests/Tests/Engines/ML Quests/Packets.cs index 8fd383e19..5001f3cfc 100644 --- a/Projects/UOContent.Tests/Tests/Engines/ML Quests/Packets.cs +++ b/Projects/UOContent.Tests/Tests/Engines/ML Quests/Packets.cs @@ -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); - } -} \ No newline at end of file + Stream.Write((short)0x2A); + Stream.Write((byte)0); + Stream.Write((byte)0xFF); + } +} diff --git a/Projects/UOContent.Tests/Tests/Engines/Party/Packets.cs b/Projects/UOContent.Tests/Tests/Engines/Party/Packets.cs index ddda0685c..0ad84a5b0 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Party/Packets.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Party/Packets.cs @@ -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); - } -} \ No newline at end of file + Stream.Write((short)0x0006); + Stream.Write((byte)0x07); + Stream.Write(leader); + } +} diff --git a/Projects/UOContent.Tests/Tests/Engines/Party/PartyPacketTests.cs b/Projects/UOContent.Tests/Tests/Engines/Party/PartyPacketTests.cs index a61a729b8..9fc00f711 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Party/PartyPacketTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Party/PartyPacketTests.cs @@ -6,92 +6,93 @@ using Xunit; namespace UOContent.Tests; -public class PartyPacketTests : IClassFixture +[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); - } -} \ No newline at end of file + } +} diff --git a/Projects/UOContent.Tests/Tests/Engines/Veteran Rewards/Character Statue Maker/CharacterStatuePacketTests.cs b/Projects/UOContent.Tests/Tests/Engines/Veteran Rewards/Character Statue Maker/CharacterStatuePacketTests.cs index 42f43b207..c8f6bedab 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Veteran Rewards/Character Statue Maker/CharacterStatuePacketTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Veteran Rewards/Character Statue Maker/CharacterStatuePacketTests.cs @@ -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); - } -} \ No newline at end of file + } +} diff --git a/Projects/UOContent.Tests/Tests/Engines/Veteran Rewards/Character Statue Maker/Packets.cs b/Projects/UOContent.Tests/Tests/Engines/Veteran Rewards/Character Statue Maker/Packets.cs index a98e69cbc..d28be2e51 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Veteran Rewards/Character Statue Maker/Packets.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Veteran Rewards/Character Statue Maker/Packets.cs @@ -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); - } -} \ No newline at end of file + 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); + } +} diff --git a/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs b/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs index 06679a48e..85f5ec732 100644 --- a/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs +++ b/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs @@ -35,6 +35,6 @@ public class StaticLayoutTestGump : StaticGump protected override void BuildStrings(ref GumpStringsBuilder builder) { - builder.SetStringSlot("petName", $"
{_petName}
"); + builder.SetHtmlTextCentered("petName", _petName); } } diff --git a/Projects/UOContent.Tests/Tests/Gumps/TestLayoutGumps.cs b/Projects/UOContent.Tests/Tests/Gumps/TestLayoutGumps.cs index 465f08c80..7cd8e67d1 100644 --- a/Projects/UOContent.Tests/Tests/Gumps/TestLayoutGumps.cs +++ b/Projects/UOContent.Tests/Tests/Gumps/TestLayoutGumps.cs @@ -7,7 +7,7 @@ using Xunit; namespace Server.Tests.Gumps; -[Collection("Sequential Tests")] +[Collection("Sequential UOContent Tests")] public class TestLayoutGumps { [Fact] diff --git a/Projects/UOContent.Tests/Tests/Items/Books/BookPacketTests.cs b/Projects/UOContent.Tests/Tests/Items/Books/BookPacketTests.cs index d8025bcf4..ed1903f3b 100644 --- a/Projects/UOContent.Tests/Tests/Items/Books/BookPacketTests.cs +++ b/Projects/UOContent.Tests/Tests/Items/Books/BookPacketTests.cs @@ -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 +[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); - } + } } diff --git a/Projects/UOContent.Tests/Tests/Items/Books/Packets.cs b/Projects/UOContent.Tests/Tests/Items/Books/Packets.cs index c098729c6..4a7b175d3 100644 --- a/Projects/UOContent.Tests/Tests/Items/Books/Packets.cs +++ b/Projects/UOContent.Tests/Tests/Items/Books/Packets.cs @@ -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 - } -} \ No newline at end of file + Stream.Write((ushort)(authorBuffer.Length + 1)); + Stream.Write(authorBuffer, 0, authorBuffer.Length); + Stream.Write((byte)0); // terminate + } +} diff --git a/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/BulletinBoardPacketTests.cs b/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/BulletinBoardPacketTests.cs index cf2719d89..db6910080 100644 --- a/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/BulletinBoardPacketTests.cs +++ b/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/BulletinBoardPacketTests.cs @@ -7,8 +7,8 @@ using Xunit; namespace UOContent.Tests; -[Collection("Sequential Tests")] -public class BulletinBoardPacketTests : IClassFixture +[Collection("Sequential UOContent Tests")] +public class BulletinBoardPacketTests { [Theory] [InlineData("Test Name")] @@ -16,16 +16,16 @@ public class BulletinBoardPacketTests : IClassFixture [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 [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) { - } -} \ No newline at end of file + } +} diff --git a/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/Packets.cs b/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/Packets.cs index 2776a3a4f..a6badc78e 100644 --- a/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/Packets.cs +++ b/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/Packets.cs @@ -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; +} diff --git a/Projects/UOContent.Tests/Tests/Items/Games/Mahjong/MahjongPacketTests.cs b/Projects/UOContent.Tests/Tests/Items/Games/Mahjong/MahjongPacketTests.cs index d5e230c68..c141f7397 100644 --- a/Projects/UOContent.Tests/Tests/Items/Games/Mahjong/MahjongPacketTests.cs +++ b/Projects/UOContent.Tests/Tests/Items/Games/Mahjong/MahjongPacketTests.cs @@ -6,42 +6,42 @@ using Xunit; namespace UOContent.Tests; -[Collection("Sequential Tests")] -public class MahjongPacketTests : IClassFixture +[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 [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); - } -} \ No newline at end of file + } +} diff --git a/Projects/UOContent.Tests/Tests/Items/Games/Mahjong/Packets.cs b/Projects/UOContent.Tests/Tests/Items/Games/Mahjong/Packets.cs index 6e9f83a80..d6fd17f74 100644 --- a/Projects/UOContent.Tests/Tests/Items/Games/Mahjong/Packets.cs +++ b/Projects/UOContent.Tests/Tests/Items/Games/Mahjong/Packets.cs @@ -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); - } -} \ No newline at end of file + Stream.Write(game); + Stream.Write((byte)0); + Stream.Write((byte)0x1A); + } +} diff --git a/Projects/UOContent.Tests/Tests/Items/Maps/Packets.cs b/Projects/UOContent.Tests/Tests/Items/Maps/Packets.cs index 5198abcbf..07c1e7f4f 100644 --- a/Projects/UOContent.Tests/Tests/Items/Maps/Packets.cs +++ b/Projects/UOContent.Tests/Tests/Items/Maps/Packets.cs @@ -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) { - } -} \ No newline at end of file + } +} diff --git a/Projects/UOContent.Tests/Tests/Items/Maps/TestMapItemPackets.cs b/Projects/UOContent.Tests/Tests/Items/Maps/TestMapItemPackets.cs index 67e1a6188..8462dc3a3 100644 --- a/Projects/UOContent.Tests/Tests/Items/Maps/TestMapItemPackets.cs +++ b/Projects/UOContent.Tests/Tests/Items/Maps/TestMapItemPackets.cs @@ -7,26 +7,26 @@ using Xunit; namespace UOContent.Tests; -[Collection("Sequential Tests")] -public class TestMapItemPackets : IClassFixture +[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 [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); - } -} \ No newline at end of file + } +} diff --git a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs index 874574f19..3f1d41de7 100644 --- a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs +++ b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/CorpsePacketTests.cs @@ -7,50 +7,50 @@ using Xunit; namespace UOContent.Tests; -[Collection("Sequential Tests")] -public class CorpsePacketTests : IClassFixture +[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); - } -} \ No newline at end of file + } +} diff --git a/Projects/UOContent.Tests/Tests/Items/Weapons/Abilities/WeaponAbilityPacketTests.cs b/Projects/UOContent.Tests/Tests/Items/Weapons/Abilities/WeaponAbilityPacketTests.cs index ecc1b4115..81140a6f4 100644 --- a/Projects/UOContent.Tests/Tests/Items/Weapons/Abilities/WeaponAbilityPacketTests.cs +++ b/Projects/UOContent.Tests/Tests/Items/Weapons/Abilities/WeaponAbilityPacketTests.cs @@ -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); - } -} \ No newline at end of file + } +} diff --git a/Projects/UOContent.Tests/Tests/Items/Weapons/Abilities/WeaponAbilityPackets.cs b/Projects/UOContent.Tests/Tests/Items/Weapons/Abilities/WeaponAbilityPackets.cs index 5a8592f08..adae27c5b 100644 --- a/Projects/UOContent.Tests/Tests/Items/Weapons/Abilities/WeaponAbilityPackets.cs +++ b/Projects/UOContent.Tests/Tests/Items/Weapons/Abilities/WeaponAbilityPackets.cs @@ -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); - } -} \ No newline at end of file + Stream.Write((short)0x21); + } +} diff --git a/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs b/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs new file mode 100644 index 000000000..ab0971904 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs @@ -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)); + } +} diff --git a/Projects/UOContent.Tests/Tests/Misc/ProfanityProtectionTests.cs b/Projects/UOContent.Tests/Tests/Misc/ProfanityProtectionTests.cs new file mode 100644 index 000000000..012d649b8 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Misc/ProfanityProtectionTests.cs @@ -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 + )); + } +} diff --git a/Projects/UOContent.Tests/Tests/Multis/Boats/BoatPacketTests.cs b/Projects/UOContent.Tests/Tests/Multis/Boats/BoatPacketTests.cs index 01fdddd77..29ecf5887 100644 --- a/Projects/UOContent.Tests/Tests/Multis/Boats/BoatPacketTests.cs +++ b/Projects/UOContent.Tests/Tests/Multis/Boats/BoatPacketTests.cs @@ -11,7 +11,8 @@ using Xunit; namespace UOContent.Tests; -public class BoatPacketTests : IClassFixture +[Collection("Sequential UOContent Tests")] +public class BoatPacketTests { [Theory] [InlineData(Direction.West, 10, 100, 200)] diff --git a/Projects/UOContent.Tests/Tests/Multis/Boats/Packets.cs b/Projects/UOContent.Tests/Tests/Multis/Boats/Packets.cs index 1dcd9adf4..2f8baf560 100644 --- a/Projects/UOContent.Tests/Tests/Multis/Boats/Packets.cs +++ b/Projects/UOContent.Tests/Tests/Multis/Boats/Packets.cs @@ -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; } -} \ No newline at end of file + + Stream.Seek(3, SeekOrigin.Begin); + Stream.Write((short)count); + } +} diff --git a/Projects/UOContent.Tests/Tests/Multis/Houses/HousePacketTests.cs b/Projects/UOContent.Tests/Tests/Multis/Houses/HousePacketTests.cs index 66d36bbd8..cd49ed2e7 100644 --- a/Projects/UOContent.Tests/Tests/Multis/Houses/HousePacketTests.cs +++ b/Projects/UOContent.Tests/Tests/Multis/Houses/HousePacketTests.cs @@ -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 + ); } -} \ No newline at end of file + 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); + } +} diff --git a/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs b/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs index 2290178a7..5bd132a13 100644 --- a/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs +++ b/Projects/UOContent.Tests/Tests/Multis/Houses/HousePackets.cs @@ -3,320 +3,319 @@ using System.Buffers; using System.IO; using Server.Compression; -namespace Server.Network +namespace Server.Network; + +public class BeginHouseCustomization : Packet { - public class BeginHouseCustomization : Packet + public BeginHouseCustomization(Serial house) : base(0xBF) { - public BeginHouseCustomization(Serial house) : base(0xBF) - { - EnsureCapacity(17); + EnsureCapacity(17); - Stream.Write((short)0x20); - Stream.Write(house); - Stream.Write((byte)0x04); - Stream.Write((ushort)0x0000); - Stream.Write((ushort)0xFFFF); - Stream.Write((ushort)0xFFFF); - Stream.Write((byte)0xFF); + Stream.Write((short)0x20); + Stream.Write(house); + Stream.Write((byte)0x04); + Stream.Write((ushort)0x0000); + Stream.Write((ushort)0xFFFF); + Stream.Write((ushort)0xFFFF); + Stream.Write((byte)0xFF); + } +} + +public class EndHouseCustomization : Packet +{ + public EndHouseCustomization(Serial house) : base(0xBF) + { + EnsureCapacity(17); + + Stream.Write((short)0x20); + Stream.Write(house); + Stream.Write((byte)0x05); + Stream.Write((ushort)0x0000); + Stream.Write((ushort)0xFFFF); + Stream.Write((ushort)0xFFFF); + Stream.Write((byte)0xFF); + } +} + +public sealed class DesignStateGeneral : Packet +{ + public DesignStateGeneral(Serial house, int revision) : base(0xBF) + { + EnsureCapacity(13); + + Stream.Write((short)0x1D); + Stream.Write(house); + Stream.Write(revision); + } +} + +public sealed class DesignStateDetailed : Packet +{ + public const int MaxItemsPerStairBuffer = 750; + + private readonly bool[] m_PlaneUsed = new bool[9]; + private readonly byte[] m_PrimBuffer = new byte[4]; + + public DesignStateDetailed(Serial serial, int revision, int xMin, int yMin, int xMax, int yMax, MultiTileEntry[] tiles) + : base(0xD8) + { + EnsureCapacity(17 + tiles.Length * 5); + + Write((byte)0x03); // Compression Type + Write((byte)0x00); // Unknown + Write(serial.Value); + Write(revision); + Write((short)tiles.Length); + Write((short)0); // Buffer length : reserved + Write((byte)0); // Plane count : reserved + + var totalLength = 1; // includes plane count + + var width = xMax - xMin + 1; + var height = yMax - yMin + 1; + + var planeBuffers = new byte[9][]; + + for (var i = 0; i < planeBuffers.Length; ++i) + { + planeBuffers[i] = ArrayPool.Shared.Rent(0x400); } + + var stairBuffers = new byte[6][]; + + for (var i = 0; i < stairBuffers.Length; ++i) + { + stairBuffers[i] = ArrayPool.Shared.Rent(MaxItemsPerStairBuffer * 5); + } + + Clear(planeBuffers[0], width * height * 2); + + for (var i = 0; i < 4; ++i) + { + Clear(planeBuffers[1 + i], (width - 1) * (height - 2) * 2); + Clear(planeBuffers[5 + i], width * (height - 1) * 2); + } + + var totalStairsUsed = 0; + + for (var i = 0; i < tiles.Length; ++i) + { + var mte = tiles[i]; + var x = mte.OffsetX - xMin; + var y = mte.OffsetY - yMin; + int z = mte.OffsetZ; + var floor = TileData.ItemTable[mte.ItemId & TileData.MaxItemValue].Height <= 0; + int plane, size; + + switch (z) + { + case 0: + plane = 0; + break; + case 7: + plane = 1; + break; + case 27: + plane = 2; + break; + case 47: + plane = 3; + break; + case 67: + plane = 4; + break; + default: + { + var stairBufferIndex = totalStairsUsed / MaxItemsPerStairBuffer; + var stairBuffer = stairBuffers[stairBufferIndex]; + + var byteIndex = totalStairsUsed % MaxItemsPerStairBuffer * 5; + + stairBuffer[byteIndex++] = (byte)(mte.ItemId >> 8); + stairBuffer[byteIndex++] = (byte)mte.ItemId; + + stairBuffer[byteIndex++] = (byte)mte.OffsetX; + stairBuffer[byteIndex++] = (byte)mte.OffsetY; + stairBuffer[byteIndex] = (byte)mte.OffsetZ; + + ++totalStairsUsed; + + continue; + } + } + + if (plane == 0) + { + size = height; + } + else if (floor) + { + size = height - 2; + x -= 1; + y -= 1; + } + else + { + size = height - 1; + plane += 4; + } + + var index = (x * size + y) * 2; + + if (x < 0 || y < 0 || y >= size || index + 1 >= 0x400) + { + var stairBufferIndex = totalStairsUsed / MaxItemsPerStairBuffer; + var stairBuffer = stairBuffers[stairBufferIndex]; + + var byteIndex = totalStairsUsed % MaxItemsPerStairBuffer * 5; + + stairBuffer[byteIndex++] = (byte)(mte.ItemId >> 8); + stairBuffer[byteIndex++] = (byte)mte.ItemId; + + stairBuffer[byteIndex++] = (byte)mte.OffsetX; + stairBuffer[byteIndex++] = (byte)mte.OffsetY; + stairBuffer[byteIndex] = (byte)mte.OffsetZ; + + ++totalStairsUsed; + } + else + { + m_PlaneUsed[plane] = true; + planeBuffers[plane][index] = (byte)(mte.ItemId >> 8); + planeBuffers[plane][index + 1] = (byte)mte.ItemId; + } + } + + var planeCount = 0; + + var deflatedBuffer = ArrayPool.Shared.Rent(0x2000); + + for (var i = 0; i < planeBuffers.Length; ++i) + { + if (!m_PlaneUsed[i]) + { + ArrayPool.Shared.Return(planeBuffers[i]); + continue; + } + + ++planeCount; + + int size = i switch + { + 0 => width * height * 2, + < 5 => (width - 1) * (height - 2) * 2, + _ => width * (height - 1) * 2 + }; + + var inflatedBuffer = planeBuffers[i]; + + var deflatedLength = Deflate.Standard.Pack( + deflatedBuffer, + inflatedBuffer.AsSpan(0, size) + ); + + if (deflatedLength == 0) + { + Console.WriteLine("Compression error"); + } + + Write((byte)(0x20 | i)); + Write((byte)size); + Write((byte)deflatedLength); + Write((byte)(((size >> 4) & 0xF0) | ((deflatedLength >> 8) & 0xF))); + Write(deflatedBuffer, 0, deflatedLength); + + totalLength += 4 + deflatedLength; + ArrayPool.Shared.Return(inflatedBuffer); + } + + var totalStairBuffersUsed = (totalStairsUsed + (MaxItemsPerStairBuffer - 1)) / MaxItemsPerStairBuffer; + + for (var i = 0; i < totalStairBuffersUsed; ++i) + { + ++planeCount; + + var count = Math.Min(MaxItemsPerStairBuffer, totalStairsUsed - i * MaxItemsPerStairBuffer); + + var size = count * 5; + + var inflatedBuffer = stairBuffers[i]; + + var deflatedLength = Deflate.Standard.Pack( + deflatedBuffer, + inflatedBuffer.AsSpan(0, size) + ); + + if (deflatedLength == 0) + { + Console.WriteLine("Compression error"); + } + + Write((byte)(9 + i)); + Write((byte)size); + Write((byte)deflatedLength); + Write((byte)(((size >> 4) & 0xF0) | ((deflatedLength >> 8) & 0xF))); + Write(deflatedBuffer, 0, deflatedLength); + + totalLength += 4 + deflatedLength; + } + + for (var i = 0; i < stairBuffers.Length; ++i) + { + ArrayPool.Shared.Return(stairBuffers[i]); + } + + ArrayPool.Shared.Return(deflatedBuffer); + + Stream.Seek(15, SeekOrigin.Begin); + + Write((short)totalLength); // Buffer length + Write((byte)planeCount); // Plane count } - public class EndHouseCustomization : Packet + public void Write(int value) { - public EndHouseCustomization(Serial house) : base(0xBF) - { - EnsureCapacity(17); + m_PrimBuffer[0] = (byte)(value >> 24); + m_PrimBuffer[1] = (byte)(value >> 16); + m_PrimBuffer[2] = (byte)(value >> 8); + m_PrimBuffer[3] = (byte)value; - Stream.Write((short)0x20); - Stream.Write(house); - Stream.Write((byte)0x05); - Stream.Write((ushort)0x0000); - Stream.Write((ushort)0xFFFF); - Stream.Write((ushort)0xFFFF); - Stream.Write((byte)0xFF); - } + Stream.UnderlyingStream.Write(m_PrimBuffer, 0, 4); } - public sealed class DesignStateGeneral : Packet + public void Write(uint value) { - public DesignStateGeneral(Serial house, int revision) : base(0xBF) - { - EnsureCapacity(13); + m_PrimBuffer[0] = (byte)(value >> 24); + m_PrimBuffer[1] = (byte)(value >> 16); + m_PrimBuffer[2] = (byte)(value >> 8); + m_PrimBuffer[3] = (byte)value; - Stream.Write((short)0x1D); - Stream.Write(house); - Stream.Write(revision); - } + Stream.UnderlyingStream.Write(m_PrimBuffer, 0, 4); } - public sealed class DesignStateDetailed : Packet + public void Write(short value) { - public const int MaxItemsPerStairBuffer = 750; + m_PrimBuffer[0] = (byte)(value >> 8); + m_PrimBuffer[1] = (byte)value; - private readonly bool[] m_PlaneUsed = new bool[9]; - private readonly byte[] m_PrimBuffer = new byte[4]; + Stream.UnderlyingStream.Write(m_PrimBuffer, 0, 2); + } - public DesignStateDetailed(Serial serial, int revision, int xMin, int yMin, int xMax, int yMax, MultiTileEntry[] tiles) - : base(0xD8) + public void Write(byte value) + { + Stream.UnderlyingStream.WriteByte(value); + } + + public void Write(byte[] buffer, int offset, int size) + { + Stream.UnderlyingStream.Write(buffer, offset, size); + } + + public static void Clear(byte[] buffer, int size) + { + for (var i = 0; i < size; ++i) { - EnsureCapacity(17 + tiles.Length * 5); - - Write((byte)0x03); // Compression Type - Write((byte)0x00); // Unknown - Write(serial.Value); - Write(revision); - Write((short)tiles.Length); - Write((short)0); // Buffer length : reserved - Write((byte)0); // Plane count : reserved - - var totalLength = 1; // includes plane count - - var width = xMax - xMin + 1; - var height = yMax - yMin + 1; - - var planeBuffers = new byte[9][]; - - for (var i = 0; i < planeBuffers.Length; ++i) - { - planeBuffers[i] = ArrayPool.Shared.Rent(0x400); - } - - var stairBuffers = new byte[6][]; - - for (var i = 0; i < stairBuffers.Length; ++i) - { - stairBuffers[i] = ArrayPool.Shared.Rent(MaxItemsPerStairBuffer * 5); - } - - Clear(planeBuffers[0], width * height * 2); - - for (var i = 0; i < 4; ++i) - { - Clear(planeBuffers[1 + i], (width - 1) * (height - 2) * 2); - Clear(planeBuffers[5 + i], width * (height - 1) * 2); - } - - var totalStairsUsed = 0; - - for (var i = 0; i < tiles.Length; ++i) - { - var mte = tiles[i]; - var x = mte.OffsetX - xMin; - var y = mte.OffsetY - yMin; - int z = mte.OffsetZ; - var floor = TileData.ItemTable[mte.ItemId & TileData.MaxItemValue].Height <= 0; - int plane, size; - - switch (z) - { - case 0: - plane = 0; - break; - case 7: - plane = 1; - break; - case 27: - plane = 2; - break; - case 47: - plane = 3; - break; - case 67: - plane = 4; - break; - default: - { - var stairBufferIndex = totalStairsUsed / MaxItemsPerStairBuffer; - var stairBuffer = stairBuffers[stairBufferIndex]; - - var byteIndex = totalStairsUsed % MaxItemsPerStairBuffer * 5; - - stairBuffer[byteIndex++] = (byte)(mte.ItemId >> 8); - stairBuffer[byteIndex++] = (byte)mte.ItemId; - - stairBuffer[byteIndex++] = (byte)mte.OffsetX; - stairBuffer[byteIndex++] = (byte)mte.OffsetY; - stairBuffer[byteIndex] = (byte)mte.OffsetZ; - - ++totalStairsUsed; - - continue; - } - } - - if (plane == 0) - { - size = height; - } - else if (floor) - { - size = height - 2; - x -= 1; - y -= 1; - } - else - { - size = height - 1; - plane += 4; - } - - var index = (x * size + y) * 2; - - if (x < 0 || y < 0 || y >= size || index + 1 >= 0x400) - { - var stairBufferIndex = totalStairsUsed / MaxItemsPerStairBuffer; - var stairBuffer = stairBuffers[stairBufferIndex]; - - var byteIndex = totalStairsUsed % MaxItemsPerStairBuffer * 5; - - stairBuffer[byteIndex++] = (byte)(mte.ItemId >> 8); - stairBuffer[byteIndex++] = (byte)mte.ItemId; - - stairBuffer[byteIndex++] = (byte)mte.OffsetX; - stairBuffer[byteIndex++] = (byte)mte.OffsetY; - stairBuffer[byteIndex] = (byte)mte.OffsetZ; - - ++totalStairsUsed; - } - else - { - m_PlaneUsed[plane] = true; - planeBuffers[plane][index] = (byte)(mte.ItemId >> 8); - planeBuffers[plane][index + 1] = (byte)mte.ItemId; - } - } - - var planeCount = 0; - - var deflatedBuffer = ArrayPool.Shared.Rent(0x2000); - - for (var i = 0; i < planeBuffers.Length; ++i) - { - if (!m_PlaneUsed[i]) - { - ArrayPool.Shared.Return(planeBuffers[i]); - continue; - } - - ++planeCount; - - int size = i switch - { - 0 => width * height * 2, - < 5 => (width - 1) * (height - 2) * 2, - _ => width * (height - 1) * 2 - }; - - var inflatedBuffer = planeBuffers[i]; - - var deflatedLength = Deflate.Standard.Pack( - deflatedBuffer, - inflatedBuffer.AsSpan(0, size) - ); - - if (deflatedLength == 0) - { - Console.WriteLine("Compression error"); - } - - Write((byte)(0x20 | i)); - Write((byte)size); - Write((byte)deflatedLength); - Write((byte)(((size >> 4) & 0xF0) | ((deflatedLength >> 8) & 0xF))); - Write(deflatedBuffer, 0, deflatedLength); - - totalLength += 4 + deflatedLength; - ArrayPool.Shared.Return(inflatedBuffer); - } - - var totalStairBuffersUsed = (totalStairsUsed + (MaxItemsPerStairBuffer - 1)) / MaxItemsPerStairBuffer; - - for (var i = 0; i < totalStairBuffersUsed; ++i) - { - ++planeCount; - - var count = Math.Min(MaxItemsPerStairBuffer, totalStairsUsed - i * MaxItemsPerStairBuffer); - - var size = count * 5; - - var inflatedBuffer = stairBuffers[i]; - - var deflatedLength = Deflate.Standard.Pack( - deflatedBuffer, - inflatedBuffer.AsSpan(0, size) - ); - - if (deflatedLength == 0) - { - Console.WriteLine("Compression error"); - } - - Write((byte)(9 + i)); - Write((byte)size); - Write((byte)deflatedLength); - Write((byte)(((size >> 4) & 0xF0) | ((deflatedLength >> 8) & 0xF))); - Write(deflatedBuffer, 0, deflatedLength); - - totalLength += 4 + deflatedLength; - } - - for (var i = 0; i < stairBuffers.Length; ++i) - { - ArrayPool.Shared.Return(stairBuffers[i]); - } - - ArrayPool.Shared.Return(deflatedBuffer); - - Stream.Seek(15, SeekOrigin.Begin); - - Write((short)totalLength); // Buffer length - Write((byte)planeCount); // Plane count - } - - public void Write(int value) - { - m_PrimBuffer[0] = (byte)(value >> 24); - m_PrimBuffer[1] = (byte)(value >> 16); - m_PrimBuffer[2] = (byte)(value >> 8); - m_PrimBuffer[3] = (byte)value; - - Stream.UnderlyingStream.Write(m_PrimBuffer, 0, 4); - } - - public void Write(uint value) - { - m_PrimBuffer[0] = (byte)(value >> 24); - m_PrimBuffer[1] = (byte)(value >> 16); - m_PrimBuffer[2] = (byte)(value >> 8); - m_PrimBuffer[3] = (byte)value; - - Stream.UnderlyingStream.Write(m_PrimBuffer, 0, 4); - } - - public void Write(short value) - { - m_PrimBuffer[0] = (byte)(value >> 8); - m_PrimBuffer[1] = (byte)value; - - Stream.UnderlyingStream.Write(m_PrimBuffer, 0, 2); - } - - public void Write(byte value) - { - Stream.UnderlyingStream.WriteByte(value); - } - - public void Write(byte[] buffer, int offset, int size) - { - Stream.UnderlyingStream.Write(buffer, offset, size); - } - - public static void Clear(byte[] buffer, int size) - { - for (var i = 0; i < size; ++i) - { - buffer[i] = 0; - } + buffer[i] = 0; } } } diff --git a/Projects/UOContent.Tests/Tests/Network/Packets/ArrowPacketTests.cs b/Projects/UOContent.Tests/Tests/Network/Packets/ArrowPacketTests.cs index 0f1b01b39..b85f34887 100644 --- a/Projects/UOContent.Tests/Tests/Network/Packets/ArrowPacketTests.cs +++ b/Projects/UOContent.Tests/Tests/Network/Packets/ArrowPacketTests.cs @@ -8,14 +8,14 @@ public class ArrowPacketTests [Fact] public void TestCancelArrow() { - var expected = new CancelArrow().Compile(); + var expected = new CancelArrow().Compile(); - var ns = PacketTestUtilities.CreateTestNetState(); - ns.SendCancelArrow(0, 0, Serial.Zero); + var ns = PacketTestUtilities.CreateTestNetState(); + ns.SendCancelArrow(0, 0, Serial.Zero); var result = ns.SendPipe.Reader.AvailableToRead(); AssertThat.Equal(result, expected); - } + } [Theory] [InlineData(0, 0)] @@ -23,14 +23,14 @@ public class ArrowPacketTests [InlineData(100000, 100000)] public void TestSetArrow(int x, int y) { - var expected = new SetArrow(x, y).Compile(); + var expected = new SetArrow(x, y).Compile(); - var ns = PacketTestUtilities.CreateTestNetState(); - ns.SendSetArrow(x, y, Serial.Zero); + var ns = PacketTestUtilities.CreateTestNetState(); + ns.SendSetArrow(x, y, Serial.Zero); var result = ns.SendPipe.Reader.AvailableToRead(); AssertThat.Equal(result, expected); - } + } [Theory] [InlineData(0, 0)] @@ -38,17 +38,17 @@ public class ArrowPacketTests [InlineData(100000, 100000)] public void TestCancelArrowHS(int x, int y) { - Serial serial = (Serial)0x1024; + Serial serial = (Serial)0x1024; - var expected = new CancelArrowHS(x, y, serial).Compile(); + var expected = new CancelArrowHS(x, y, serial).Compile(); - var ns = PacketTestUtilities.CreateTestNetState(); - ns.ProtocolChanges = ProtocolChanges.HighSeas; - ns.SendCancelArrow(x, y, serial); + var ns = PacketTestUtilities.CreateTestNetState(); + ns.ProtocolChanges = ProtocolChanges.HighSeas; + ns.SendCancelArrow(x, y, serial); var result = ns.SendPipe.Reader.AvailableToRead(); AssertThat.Equal(result, expected); - } + } [Theory] [InlineData(0, 0)] @@ -56,15 +56,15 @@ public class ArrowPacketTests [InlineData(100000, 100000)] public void TestSetArrowHS(int x, int y) { - Serial serial = (Serial)0x1024; + Serial serial = (Serial)0x1024; - var expected = new SetArrowHS(x, y, serial).Compile(); + var expected = new SetArrowHS(x, y, serial).Compile(); - var ns = PacketTestUtilities.CreateTestNetState(); - ns.ProtocolChanges = ProtocolChanges.HighSeas; - ns.SendSetArrow(x, y, serial); + var ns = PacketTestUtilities.CreateTestNetState(); + ns.ProtocolChanges = ProtocolChanges.HighSeas; + ns.SendSetArrow(x, y, serial); var result = ns.SendPipe.Reader.AvailableToRead(); AssertThat.Equal(result, expected); - } -} \ No newline at end of file + } +} diff --git a/Projects/UOContent.Tests/Tests/Network/Packets/ArrowPackets.cs b/Projects/UOContent.Tests/Tests/Network/Packets/ArrowPackets.cs index 5fea8c156..cb5b0019a 100644 --- a/Projects/UOContent.Tests/Tests/Network/Packets/ArrowPackets.cs +++ b/Projects/UOContent.Tests/Tests/Network/Packets/ArrowPackets.cs @@ -4,40 +4,40 @@ public sealed class CancelArrow : Packet { public CancelArrow() : base(0xBA, 6) { - Stream.Write((byte)0); - Stream.Write((short)-1); - Stream.Write((short)-1); - } + Stream.Write((byte)0); + Stream.Write((short)-1); + Stream.Write((short)-1); + } } public sealed class SetArrow : Packet { public SetArrow(int x, int y) : base(0xBA, 6) { - Stream.Write((byte)1); - Stream.Write((short)x); - Stream.Write((short)y); - } + Stream.Write((byte)1); + Stream.Write((short)x); + Stream.Write((short)y); + } } public sealed class CancelArrowHS : Packet { public CancelArrowHS(int x, int y, Serial s) : base(0xBA, 10) { - Stream.Write((byte)0); - Stream.Write((short)x); - Stream.Write((short)y); - Stream.Write(s); - } + Stream.Write((byte)0); + Stream.Write((short)x); + Stream.Write((short)y); + Stream.Write(s); + } } public sealed class SetArrowHS : Packet { public SetArrowHS(int x, int y, Serial s) : base(0xBA, 10) { - Stream.Write((byte)1); - Stream.Write((short)x); - Stream.Write((short)y); - Stream.Write(s); - } -} \ No newline at end of file + Stream.Write((byte)1); + Stream.Write((short)x); + Stream.Write((short)y); + Stream.Write(s); + } +} diff --git a/Projects/UOContent.Tests/Tests/Network/Packets/BuffIconPackets.cs b/Projects/UOContent.Tests/Tests/Network/Packets/BuffIconPackets.cs index 9910ffaa7..35a677ae0 100644 --- a/Projects/UOContent.Tests/Tests/Network/Packets/BuffIconPackets.cs +++ b/Projects/UOContent.Tests/Tests/Network/Packets/BuffIconPackets.cs @@ -1,84 +1,83 @@ using System; using Server.Engines.BuffIcons; -namespace Server.Network +namespace Server.Network; + +public sealed class AddBuffPacket : Packet { - public sealed class AddBuffPacket : Packet - { - public AddBuffPacket(Serial m, BuffInfo info) - : this( - m, - info.ID, - info.TitleCliloc, - info.SecondaryCliloc, - info.Args, - info.Duration - ) - { - } - - public AddBuffPacket( - Serial mob, BuffIcon iconID, int titleCliloc, int secondaryCliloc, TextDefinition args, - TimeSpan length + public AddBuffPacket(Serial m, BuffInfo info) + : this( + m, + info.ID, + info.TitleCliloc, + info.SecondaryCliloc, + info.Args, + info.Duration ) - : base(0xDF) - { - var hasArgs = args != null; - - EnsureCapacity(hasArgs ? 48 + args.ToString().Length * 2 : 44); - Stream.Write(mob); - - Stream.Write((short)iconID); // ID - Stream.Write((short)0x1); // Type 0 for removal. 1 for add 2 for Data - - Stream.Fill(4); - - Stream.Write((short)iconID); // ID - Stream.Write((short)0x01); // Type 0 for removal. 1 for add 2 for Data - - Stream.Fill(4); - - Stream.Write((short)Math.Max(length.TotalSeconds, 0)); // Time in seconds - - Stream.Fill(3); - Stream.Write(titleCliloc); - Stream.Write(secondaryCliloc); - - if (!hasArgs) - { - // m_Stream.Fill( 2 ); - Stream.Fill(10); - } - else - { - Stream.Fill(4); - Stream.Write((short)0x1); // Unknown -> Possibly something saying 'hey, I have more data!'? - Stream.Fill(2); - - // m_Stream.WriteLittleUniNull( "\t#1018280" ); - Stream.WriteLittleUniNull($"\t{args}"); - - Stream.Write((short)0x1); // Even more Unknown -> Possibly something saying 'hey, I have more data!'? - Stream.Fill(2); - } - } + { } - public sealed class RemoveBuffPacket : Packet + public AddBuffPacket( + Serial mob, BuffIcon iconID, int titleCliloc, int secondaryCliloc, TextDefinition args, + TimeSpan length + ) + : base(0xDF) { - public RemoveBuffPacket(Serial mob, BuffInfo info) : this(mob, info.ID) + var hasArgs = args != null; + + EnsureCapacity(hasArgs ? 48 + args.ToString().Length * 2 : 44); + Stream.Write(mob); + + Stream.Write((short)iconID); // ID + Stream.Write((short)0x1); // Type 0 for removal. 1 for add 2 for Data + + Stream.Fill(4); + + Stream.Write((short)iconID); // ID + Stream.Write((short)0x01); // Type 0 for removal. 1 for add 2 for Data + + Stream.Fill(4); + + Stream.Write((short)Math.Max(length.TotalSeconds, 0)); // Time in seconds + + Stream.Fill(3); + Stream.Write(titleCliloc); + Stream.Write(secondaryCliloc); + + if (!hasArgs) { + // m_Stream.Fill( 2 ); + Stream.Fill(10); } - - public RemoveBuffPacket(Serial mob, BuffIcon iconID) : base(0xDF) + else { - EnsureCapacity(13); - Stream.Write(mob); - - Stream.Write((short)iconID); // ID - Stream.Write((short)0x0); // Type 0 for removal. 1 for add 2 for Data - Stream.Fill(4); + Stream.Write((short)0x1); // Unknown -> Possibly something saying 'hey, I have more data!'? + Stream.Fill(2); + + // m_Stream.WriteLittleUniNull( "\t#1018280" ); + Stream.WriteLittleUniNull($"\t{args}"); + + Stream.Write((short)0x1); // Even more Unknown -> Possibly something saying 'hey, I have more data!'? + Stream.Fill(2); } } } + +public sealed class RemoveBuffPacket : Packet +{ + public RemoveBuffPacket(Serial mob, BuffInfo info) : this(mob, info.ID) + { + } + + public RemoveBuffPacket(Serial mob, BuffIcon iconID) : base(0xDF) + { + EnsureCapacity(13); + Stream.Write(mob); + + Stream.Write((short)iconID); // ID + Stream.Write((short)0x0); // Type 0 for removal. 1 for add 2 for Data + + Stream.Fill(4); + } +} diff --git a/Projects/UOContent.Tests/Tests/Skills/SkillPacketsTests.cs b/Projects/UOContent.Tests/Tests/Skills/SkillPacketsTests.cs index 764e25bb6..b2149841a 100644 --- a/Projects/UOContent.Tests/Tests/Skills/SkillPacketsTests.cs +++ b/Projects/UOContent.Tests/Tests/Skills/SkillPacketsTests.cs @@ -6,7 +6,8 @@ using Xunit; namespace UOContent.Tests; -public class SkillPacketsTests : IClassFixture +[Collection("Sequential UOContent Tests")] +public class SkillPacketsTests { [Theory] [InlineData(SkillName.Alchemy, 0, 1)] diff --git a/Projects/UOContent.Tests/UOContent.Tests.csproj b/Projects/UOContent.Tests/UOContent.Tests.csproj index a6c296cef..967f997cd 100644 --- a/Projects/UOContent.Tests/UOContent.Tests.csproj +++ b/Projects/UOContent.Tests/UOContent.Tests.csproj @@ -4,13 +4,14 @@ Debug;Release;Analyze - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/Projects/UOContent/Accounting/Account.cs b/Projects/UOContent/Accounting/Account.cs index 9e554fd72..c9e85de2c 100644 --- a/Projects/UOContent/Accounting/Account.cs +++ b/Projects/UOContent/Accounting/Account.cs @@ -13,6 +13,7 @@ using Server.Network; namespace Server.Accounting; +[PropertyObject] [SerializationGenerator(6)] public partial class Account : IAccount, IComparable { diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index fce134355..50ee90e81 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -1,6 +1,8 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Net; +using System.Runtime.CompilerServices; using ModernUO.CodeGeneratedEvents; using Server.Accounting; using Server.Engines.CharacterCreation; @@ -17,13 +19,13 @@ public static class AccountHandler private static int MaxAccountsPerIP; private static bool AutoAccountCreation; - private static bool RestrictDeletion = !TestCenter.Enabled; - private static TimeSpan DeleteDelay = TimeSpan.FromDays(7.0); + private static readonly bool RestrictDeletion = !TestCenter.Enabled; + private static readonly TimeSpan DeleteDelay = TimeSpan.FromDays(7.0); private static bool PasswordCommandEnabled; private static Dictionary m_IPTable; - private static char[] m_ForbiddenChars = { '<', '>', ':', '"', '/', '\\', '|', '?', '*' }; + private static readonly SearchValues ForbiddenChars = SearchValues.Create("<>:\"/\\|?*"); public static AccessLevel LockdownLevel { get; set; } @@ -240,41 +242,24 @@ public static class AccountHandler public static bool CanCreate(IPAddress ip) => !IPTable.TryGetValue(ip, out var result) || result < MaxAccountsPerIP; - private static bool IsForbiddenChar(char c) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidUsername(ReadOnlySpan username) => + username.Length > 0 && + // Usernames must not start with a space, end with a space, or end with a period + !username.StartsWith(' ') && !username.EndsWith(' ') && !username.EndsWith('.') && + // Usernames must only contain characters [0x20 -> 0x7E], and not contain any forbidden characters + !username.ContainsAnyExceptInRange((char)0x20, (char)0x7E) && + !username.ContainsAny(ForbiddenChars); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidPassword(ReadOnlySpan password) => + password.Length > 0 && + // Passwords must have characters [0x20 -> 0x7E] + !password.ContainsAnyExceptInRange((char)0x20, (char)0x7E); + + private static Account CreateAccount(NetState state, string username, string password) { - for (var i = 0; i < m_ForbiddenChars.Length; ++i) - { - if (c == m_ForbiddenChars[i]) - { - return true; - } - } - - return false; - } - - private static Account CreateAccount(NetState state, string un, string pw) - { - if (un.Length == 0 || pw.Length == 0) - { - return null; - } - - var isSafe = !(un.StartsWithOrdinal(" ") || - un.EndsWithOrdinal(" ") || - un.EndsWithOrdinal(".")); - - for (var i = 0; isSafe && i < un.Length; ++i) - { - isSafe = un[i] >= 0x20 && un[i] < 0x7F && !IsForbiddenChar(un[i]); - } - - for (var i = 0; isSafe && i < pw.Length; ++i) - { - isSafe = pw[i] >= 0x20 && pw[i] < 0x7F; - } - - if (!isSafe) + if (!IsValidUsername(username) || !IsValidPassword(password)) { return null; } @@ -284,18 +269,16 @@ public static class AccountHandler logger.Information( $"Login: {{NetState}} Account '{{Username}}' not created, ip already has {{AccountCount}} account{(MaxAccountsPerIP == 1 ? "" : "s")}.", state, - un, + username, MaxAccountsPerIP ); return null; } - logger.Information("Login: {NetState}: Creating new account '{Username}'", state, un); + logger.Information("Login: {NetState}: Creating new account '{Username}'", state, username); - var a = new Account(un, pw); - - return a; + return new Account(username, password); } public static void EventSink_AccountLogin(AccountLoginEventArgs e) diff --git a/Projects/UOContent/Commands/Dupe.cs b/Projects/UOContent/Commands/Dupe.cs index e38a114f3..2d715714e 100644 --- a/Projects/UOContent/Commands/Dupe.cs +++ b/Projects/UOContent/Commands/Dupe.cs @@ -120,6 +120,18 @@ public static class Dupe for (var j = 0; j < src.Items.Count; j++) { + c = src.Items[j].GetType().GetConstructor(out var paramCount); + if (c == null) + { + continue; + } + + args = paramCount == 0 ? null : new object[paramCount]; + if (args != null) + { + Array.Fill(args, Type.Missing); + } + var subItem = DoDupe(src.Items[j], c, args, from); newItem.AddItem(subItem); } diff --git a/Projects/UOContent/Commands/Handlers.cs b/Projects/UOContent/Commands/Handlers.cs index de1e831e2..d1c56d339 100644 --- a/Projects/UOContent/Commands/Handlers.cs +++ b/Projects/UOContent/Commands/Handlers.cs @@ -239,7 +239,7 @@ namespace Server.Commands for (var i = 0; i < pets.Count; ++i) { - Mobile pet = pets[i]; + var pet = pets[i]; if (pet is IMount mount) { diff --git a/Projects/UOContent/Commands/Object Creation/CategorizedAddGump.cs b/Projects/UOContent/Commands/Object Creation/CategorizedAddGump.cs index b46812115..d8ea63234 100644 --- a/Projects/UOContent/Commands/Object Creation/CategorizedAddGump.cs +++ b/Projects/UOContent/Commands/Object Creation/CategorizedAddGump.cs @@ -187,7 +187,7 @@ namespace Server.Gumps Console.WriteLine("Type {0} does not have a valid item id or shrink table entry.", obj.Type); } - var bounds = ItemBounds.Table[itemID]; + var bounds = ItemBounds.Bounds[itemID]; if (itemID != 1 && bounds.Height < EntryHeight * 2) { diff --git a/Projects/UOContent/Engines/BuffIcons/BuffInfo.cs b/Projects/UOContent/Engines/BuffIcons/BuffInfo.cs index d0c1ed6c5..5a6fd5dd3 100644 --- a/Projects/UOContent/Engines/BuffIcons/BuffInfo.cs +++ b/Projects/UOContent/Engines/BuffIcons/BuffInfo.cs @@ -1,5 +1,4 @@ using System; -using ModernUO.CodeGeneratedEvents; using Server.Mobiles; namespace Server.Engines.BuffIcons; diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs index ce3c02e8c..d798a91a6 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs @@ -17,5 +17,5 @@ public class BOBEntries : GenericEntityPersistence public static void Add(IBOBEntry entity) => _bobEntriesPersistence.AddEntity(entity); - public static void Remove(IBOBEntry entity) => _bobEntriesPersistence.AddEntity(entity); + public static void Remove(IBOBEntry entity) => _bobEntriesPersistence.RemoveEntity(entity); } diff --git a/Projects/UOContent/Engines/Character Creation/CharacterCreation.cs b/Projects/UOContent/Engines/Character Creation/CharacterCreation.cs index b743b9da7..87204e7fc 100644 --- a/Projects/UOContent/Engines/Character Creation/CharacterCreation.cs +++ b/Projects/UOContent/Engines/Character Creation/CharacterCreation.cs @@ -418,7 +418,7 @@ public static partial class CharacterCreation { name = name.Trim(); - if (!NameVerification.Validate(name, 2, 16, true, false, true, 1, NameVerification.SpaceDashPeriodQuote)) + if (!NameVerification.ValidatePlayerName(name)) { name = "Generic Player"; } diff --git a/Projects/UOContent/Engines/Craft/Core/CraftGump.cs b/Projects/UOContent/Engines/Craft/Core/CraftGump.cs index c9c55bfb7..67591a289 100644 --- a/Projects/UOContent/Engines/Craft/Core/CraftGump.cs +++ b/Projects/UOContent/Engines/Craft/Core/CraftGump.cs @@ -148,11 +148,11 @@ public class CraftGump : DynamicGump if (nameNumber > 0) { - builder.AddHtmlLocalized(50, 365, 250, 18, nameNumber, resourceCount.ToString(), LabelColor); + builder.AddHtmlLocalized(50, 365, 250, 18, nameNumber, $"{resourceCount}", LabelColor); } else { - builder.AddLabel(50, 362, LabelHue, $"{nameString} ({resourceCount} Available)"); + builder.AddLabel(50, 365, LabelHue, $"{nameString} ({resourceCount} Available)"); } } // **************************************** @@ -193,7 +193,7 @@ public class CraftGump : DynamicGump if (nameNumber > 0) { - builder.AddHtmlLocalized(50, 385, 250, 18, nameNumber, resourceCount.ToString(), LabelColor); + builder.AddHtmlLocalized(50, 385, 250, 18, nameNumber, $"{resourceCount}", LabelColor); } else { @@ -279,13 +279,13 @@ public class CraftGump : DynamicGump 250, 18, subResource.Name.Number, - resourceCount.ToString(), + $"{resourceCount}", LabelColor ); } else { - builder.AddLabel(255, 60 + index * 20, LabelHue, $"{subResource.Name.String} ({resourceCount})"); + builder.AddLabel(255, 63 + index * 20, LabelHue, $"{subResource.Name.String} ({resourceCount})"); } } } @@ -334,7 +334,7 @@ public class CraftGump : DynamicGump } else { - builder.AddLabel(255, 60 + index * 20, LabelHue, craftItem.NameString); + builder.AddLabel(255, 63 + index * 20, LabelHue, craftItem.NameString); } builder.AddButton(480, 60 + index * 20, 4011, 4012, GetButtonID(4, i)); @@ -388,7 +388,7 @@ public class CraftGump : DynamicGump } else { - builder.AddLabel(255, 60 + index * 20, LabelHue, craftItem.NameString); + builder.AddLabel(255, 63 + index * 20, LabelHue, craftItem.NameString); } builder.AddButton(480, 60 + index * 20, 4011, 4012, GetButtonID(2, i)); @@ -414,7 +414,7 @@ public class CraftGump : DynamicGump } else { - builder.AddLabel(50, 80 + i * 20, LabelHue, craftGroup.NameString); + builder.AddLabel(50, 83 + i * 20, LabelHue, craftGroup.NameString); } } } diff --git a/Projects/UOContent/Engines/Doom/GenGauntlet.cs b/Projects/UOContent/Engines/Doom/GenGauntlet.cs index c770afa85..5c4c4edf2 100644 --- a/Projects/UOContent/Engines/Doom/GenGauntlet.cs +++ b/Projects/UOContent/Engines/Doom/GenGauntlet.cs @@ -163,8 +163,8 @@ namespace Server.Engines.Doom public static BaseDoor CreateDoorSet(int xDoor, int yDoor, bool doorEastToWest, int hue) { - BaseDoor hiDoor = new MetalDoor(doorEastToWest ? DoorFacing.NorthCCW : DoorFacing.WestCW); - BaseDoor loDoor = new MetalDoor(doorEastToWest ? DoorFacing.SouthCW : DoorFacing.EastCCW); + var hiDoor = new MetalDoor(doorEastToWest ? DoorFacing.NorthCCW : DoorFacing.WestCW); + var loDoor = new MetalDoor(doorEastToWest ? DoorFacing.SouthCW : DoorFacing.EastCCW); hiDoor.MoveToWorld(new Point3D(xDoor, yDoor, -1), Map.Malas); loDoor.MoveToWorld( diff --git a/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs b/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs index 8af67df58..05d5dc6dc 100644 --- a/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs +++ b/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs @@ -321,8 +321,8 @@ public partial class LeverPuzzleController : Item { for (var i = 0; i < 4; i++) { - Item l; - if ((l = GetLever(i)) != null) + var l = GetLever(i); + if (l != null) { l.ItemID = 0x108E; Effects.PlaySound(l.Location, Map, 0x3E8); @@ -361,43 +361,41 @@ public partial class LeverPuzzleController : Item { PuzzleStatus(1050004); // The circle is the key... } + else if (TheirKey == MyKey) + { + GenKey(); + Successful = GetOccupant(0); + if (Successful != null) + { + SendLocationEffect(lp_Center, 0x1153, 0, 60, 1); + PlaySounds(lp_Center, cs1); + + Effects.SendBoltEffect(Successful); + Successful.MoveToWorld(lr_Enter, Map.Malas); + + m_Timer = new LampRoomTimer(this); + m_Timer.Start(); + Enabled = false; + } + } else { - Mobile player; - if (TheirKey == MyKey) + for (var i = 0; i < 16; i++) /* Count matching SET bits, ie correct codes */ { - GenKey(); - if ((Successful = player = GetOccupant(0)) != null) + if (((MyKey >> i) & 1) == 1 && ((TheirKey >> i) & 1) == 1) { - SendLocationEffect(lp_Center, 0x1153, 0, 60, 1); - PlaySounds(lp_Center, cs1); - - Effects.SendBoltEffect(player); - player.MoveToWorld(lr_Enter, Map.Malas); - - m_Timer = new LampRoomTimer(this); - m_Timer.Start(); - Enabled = false; + correct++; } } - else + + PuzzleStatus(Statue_Msg[correct], correct > 0 ? correct.ToString() : null); + + for (var i = 0; i < 5; i++) { - for (var i = 0; i < 16; i++) /* Count matching SET bits, ie correct codes */ + var player = GetOccupant(i); + if (player != null) { - if (((MyKey >> i) & 1) == 1 && ((TheirKey >> i) & 1) == 1) - { - correct++; - } - } - - PuzzleStatus(Statue_Msg[correct], correct > 0 ? correct.ToString() : null); - - for (var i = 0; i < 5; i++) - { - if ((player = GetOccupant(i)) != null) - { - new RockTimer(player).Start(); - } + new RockTimer(player).Start(); } } } diff --git a/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleItems.cs b/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleItems.cs index 7c8362c5e..a3af7e72c 100644 --- a/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleItems.cs +++ b/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleItems.cs @@ -10,7 +10,7 @@ public partial class LampRoomBox : Item { [SerializableField(0)] private LeverPuzzleController _controller; - private Mobile _wanderer; + private WandererOfTheVoid _wanderer; public LampRoomBox(LeverPuzzleController controller) : base(0xe80) { diff --git a/Projects/UOContent/Engines/Events/AllowedDays.cs b/Projects/UOContent/Engines/Events/AllowedDays.cs new file mode 100644 index 000000000..d662a2e11 --- /dev/null +++ b/Projects/UOContent/Engines/Events/AllowedDays.cs @@ -0,0 +1,22 @@ +using System; + +namespace Server.Engines.Events; + +[Flags] +public enum AllowedDays : byte +{ + None = 0, + Sunday = 1 << 0, + Monday = 1 << 1, + Tuesday = 1 << 2, + Wednesday = 1 << 3, + Thursday = 1 << 4, + Friday = 1 << 5, + Saturday = 1 << 6, + All = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday +} + +public static class DaysOfWeekExtension +{ + public static AllowedDays ToDaysOfWeek(this DayOfWeek dayOfWeek) => (AllowedDays)(1 << (int)dayOfWeek); +} diff --git a/Projects/UOContent/Engines/Events/AllowedMonths.cs b/Projects/UOContent/Engines/Events/AllowedMonths.cs new file mode 100644 index 000000000..1103e7b9e --- /dev/null +++ b/Projects/UOContent/Engines/Events/AllowedMonths.cs @@ -0,0 +1,23 @@ +using System; + +namespace Server.Engines.Events; + +[Flags] +public enum AllowedMonths +{ + None = 0, + January = 1 << 0, + February = 1 << 1, + March = 1 << 2, + April = 1 << 3, + May = 1 << 4, + June = 1 << 5, + July = 1 << 6, + August = 1 << 7, + September = 1 << 8, + October = 1 << 9, + November = 1 << 10, + December = 1 << 11, + + All = January | February | March | April | May | June | July | August | September | October | November | December +} diff --git a/Projects/UOContent/Engines/Events/BaseScheduledEvent.cs b/Projects/UOContent/Engines/Events/BaseScheduledEvent.cs new file mode 100644 index 000000000..279de8502 --- /dev/null +++ b/Projects/UOContent/Engines/Events/BaseScheduledEvent.cs @@ -0,0 +1,67 @@ +using System; +using System.Runtime.CompilerServices; +using Server.Logging; + +namespace Server.Engines.Events; + +public abstract class BaseScheduledEvent +{ + private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseScheduledEvent)); + + public TimeZoneInfo TimeZone { get; private set; } = TimeZoneInfo.Utc; + public DateTime NextOccurrence { get; private set; } = DateTime.MaxValue; + public bool Cancelled { get; private set; } + public EventScheduler Scheduler { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Schedule(DateTime startAfter, TimeZoneInfo timeZone = null) => + Schedule(EventScheduler.Shared, startAfter, timeZone); + + public void Schedule(EventScheduler scheduler, DateTime startAfter, TimeZoneInfo timeZone = null) + { + Cancel(); + Scheduler = scheduler; + TimeZone = timeZone ?? TimeZoneInfo.Utc; + Schedule(startAfter, timeZone, true); + } + + private void Schedule(DateTime startAfter, TimeZoneInfo timeZone, bool isFirst) + { + Cancelled = false; + var afterUtc = startAfter.Kind == DateTimeKind.Utc ? startAfter : startAfter.LocalToUtc(timeZone); + + var next = GetNextOccurrence(afterUtc); + + // For the first occurrence, we should set it to the startAfter date if we have no recurrence. + NextOccurrence = next == DateTime.MaxValue && isFirst ? afterUtc : next; + + if (NextOccurrence != DateTime.MaxValue) + { + Scheduler.ScheduleEvent(this); + } + } + + protected abstract DateTime GetNextOccurrence(DateTime after); + + public void Cancel() + { + Cancelled = true; + Scheduler?.UnscheduleEvent(this); + } + + public virtual void Advance() + { + try + { + OnEvent(); + } + catch (Exception ex) + { + logger.Error(ex, "OnEvent failed to execute."); + } + + Schedule(NextOccurrence, TimeZone, false); + } + + public abstract void OnEvent(); +} diff --git a/Projects/UOContent/Engines/Events/BroadcastEvent.cs b/Projects/UOContent/Engines/Events/BroadcastEvent.cs deleted file mode 100644 index a900d263b..000000000 --- a/Projects/UOContent/Engines/Events/BroadcastEvent.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Server.Engines.Events -{ - public class BroadcastEvent : IEvent - { - private readonly int _hue; - private readonly string _text; - - public BroadcastEvent(int hue, string text) - { - _hue = hue; - _text = text; - } - - public void OnEventScheduled() - { - World.Broadcast(_hue, true, _text); - } - - public static void Initialize() - { - /* - EventScheduler.Instance.ScheduleEvent(new BroadcastEvent(22, "Test Message Please Ignore 2min"), 0, 0, TimeSpan.FromMinutes(2.0)); - EventScheduler.Instance.ScheduleEvent(new BroadcastEvent(33, "Test Message Please Ignore 3min"), 0, 0, TimeSpan.FromMinutes(3.0)); - EventScheduler.Instance.ScheduleEvent(new BroadcastEvent(44, "Test Message Please Ignore 4min"), 0, 0, TimeSpan.FromMinutes(4.0)); - */ - } - - public override string ToString() => $"Broadcast: {_text}"; - } -} diff --git a/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs b/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs new file mode 100644 index 000000000..cab4e08fe --- /dev/null +++ b/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs @@ -0,0 +1,23 @@ +using System; + +namespace Server.Engines.Events; + +public sealed class CallbackScheduledEvent : ScheduledEvent +{ + private readonly Action _callback; + + public CallbackScheduledEvent( + TimeOnly time, + Action callback, + IRecurrencePattern recurrencePattern = null + ) : base(time, recurrencePattern) => _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + + public CallbackScheduledEvent( + TimeOnly time, + DateTime endOn, + Action callback, + IRecurrencePattern recurrencePattern = null + ) : base(time, endOn, recurrencePattern) => _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + + public override void OnEvent() => _callback(); +} diff --git a/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs b/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs new file mode 100644 index 000000000..f60358f3f --- /dev/null +++ b/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs @@ -0,0 +1,204 @@ +namespace Server.Engines.Events; + +using System; + +public class HourlyRecurrencePattern : IRecurrencePattern +{ + public int IntervalHours { get; } + + public HourlyRecurrencePattern(int intervalHours = 1) => IntervalHours = Math.Max(1, intervalHours); + + public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone) + { + var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); + return new DateTime(local.Year, local.Month, local.Day, local.Hour, time.Minute, 0) + .LocalToUtc(timeZone) + .AddHours(IntervalHours); + } +} + +public class DailyRecurrencePattern : IRecurrencePattern +{ + public int IntervalDays { get; } + + public DailyRecurrencePattern(int intervalDays = 1) => IntervalDays = Math.Max(1, intervalDays); + + public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone) + { + var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); + return new DateTime(local.Year, local.Month, local.Day, time.Hour, time.Minute, 0) + .LocalToUtc(timeZone) + .AddDays(IntervalDays); + } +} + +public class WeeklyRecurrencePattern : IRecurrencePattern +{ + public int IntervalWeeks { get; } + public AllowedDays AllowedDays { get; } + public AllowedMonths AllowedMonths { get; } + + public WeeklyRecurrencePattern(int intervalWeeks = 1, AllowedMonths allowedMonths = AllowedMonths.All, AllowedDays allowedDays = AllowedDays.None) + { + IntervalWeeks = Math.Max(1, intervalWeeks); + AllowedDays = allowedDays == AllowedDays.None ? AllowedDays.All : allowedDays; + AllowedMonths = allowedMonths == AllowedMonths.None ? AllowedMonths.All : allowedMonths; + } + + public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone) + { + var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); + var daysOfWeek = AllowedDays == AllowedDays.None ? local.DayOfWeek.ToDaysOfWeek() : AllowedDays; + + var weekStart = local.Date.AddDays(-(int)local.DayOfWeek); + + // No more days in this week, jump IntervalWeeks ahead + for (var week = 0; week <= 52; week++) + { + var nextWeekStart = weekStart.AddDays(7 * IntervalWeeks * week); + var weekEnd = nextWeekStart.AddDays(6); + + var startMonth = (AllowedMonths)(1 << (nextWeekStart.Month - 1)); + var endMonth = (AllowedMonths)(1 << (weekEnd.Month - 1)); + + // Skip the entire week if the start and end months are not in the allowed months + if ((AllowedMonths & (startMonth | endMonth)) == 0) + { + continue; + } + + for (var i = 0; i < 7; i++) + { + var day = nextWeekStart.AddDays(i); + + var currentMonth = (AllowedMonths)(1 << (day.Month - 1)); + if ((AllowedMonths & currentMonth) == 0) + { + continue; + } + + var dayOfWeekFlag = (AllowedDays)(1 << (int)day.DayOfWeek); + if ((daysOfWeek & dayOfWeekFlag) != 0) + { + var candidate = new DateTime(day.Year, day.Month, day.Day, time.Hour, time.Minute, 0); + if (candidate > local && !timeZone.IsInvalidTime(candidate)) + { + return candidate.LocalToUtc(timeZone); + } + } + } + } + + return DateTime.MaxValue; + } +} + +public class MonthlyRecurrencePattern : IRecurrencePattern +{ + public int DayOfMonth { get; } + public int IntervalMonths { get; } + + public MonthlyRecurrencePattern(int dayOfMonth = -1, int intervalMonths = 1) + { + DayOfMonth = dayOfMonth != -1 ? Math.Clamp(dayOfMonth, 1, 31) : -1; + IntervalMonths = Math.Max(1, intervalMonths); + } + + public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone) + { + var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); + var year = local.Year; + var month = local.Month; + var day = DayOfMonth == -1 ? local.Day : DayOfMonth; + + for (var i = 0; i < 100; i++) + { + var nextMonth = month + IntervalMonths * i; + var candidate = new DateTime(year, 1, 1) + .AddMonths(nextMonth - 1) + .AddDays(day - 1) + .Add(time.ToTimeSpan()); + + // Some months may not have that day of the month, if not, we skip to the next interval + if (candidate > local && candidate.Day == day && !timeZone.IsInvalidTime(candidate)) + { + return candidate.LocalToUtc(timeZone); + } + } + + return DateTime.MaxValue; + } +} + +public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern +{ + public int IntervalMonths { get; } + public OrdinalDayOccurrence Ordinal { get; } + public DayOfWeek DayOfWeek { get; } + + public MonthlyOrdinalRecurrencePattern(OrdinalDayOccurrence ordinal, DayOfWeek dayOfWeek, int intervalMonths = 1) + { + IntervalMonths = Math.Max(1, intervalMonths); + Ordinal = ordinal; + DayOfWeek = dayOfWeek; + } + + public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone) + { + var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); + var year = local.Year; + var month = local.Month; + + for (var i = 0; i < 100; i++) + { + var nextMonth = month + IntervalMonths * i; + var candidateYearOffset = Math.DivRem(nextMonth - 1, 12, out var candidateMonthOffset); + var candidateYear = year + candidateYearOffset; + var candidateMonth = candidateMonthOffset + 1; + + DateTime candidate; + if (Ordinal >= OrdinalDayOccurrence.First) + { + // Find the first day of the month + var firstOfMonth = new DateTime(candidateYear, candidateMonth, 1) + .Add(time.ToTimeSpan()); + + // Find the first occurrence of the desired day + var daysOffset = ((int)DayOfWeek - (int)firstOfMonth.DayOfWeek + 7) % 7; + if (daysOffset > 7) + { + daysOffset -= 7; + } + candidate = firstOfMonth.AddDays(daysOffset + 7 * (int)Ordinal); + + // If candidate is not in the same month, skip + if (candidate.Month != candidateMonth) + { + continue; + } + } + else + { + // Find the last day of the month + var daysInMonth = DateTime.DaysInMonth(candidateYear, candidateMonth); + var lastOfMonth = new DateTime(candidateYear, candidateMonth, daysInMonth) + .Add(time.ToTimeSpan()); + + // Find the last occurrence of the desired day + var daysOffset = (int)lastOfMonth.DayOfWeek - (int)DayOfWeek + 7; + if (daysOffset >= 7) + { + daysOffset -= 7; + } + candidate = lastOfMonth.AddDays(-daysOffset); + } + + if (candidate > local && !timeZone.IsInvalidTime(candidate)) + { + return candidate.LocalToUtc(timeZone); + } + } + + return DateTime.MaxValue; + } +} diff --git a/Projects/UOContent/Engines/Events/EventScheduler.cs b/Projects/UOContent/Engines/Events/EventScheduler.cs index 363bfa320..992110371 100644 --- a/Projects/UOContent/Engines/Events/EventScheduler.cs +++ b/Projects/UOContent/Engines/Events/EventScheduler.cs @@ -1,95 +1,146 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; -namespace Server.Engines.Events +namespace Server.Engines.Events; + +public interface IRecurrencePattern { - public interface IEvent + /// + /// Get the next occurrence of the event. + /// DateTime of the next occurence in UTC or DateTime.MaxValue + /// + DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone); +} + +public enum OrdinalDayOccurrence { Last = -1, First, Second, Third, Fourth, Fifth } + +public class EventScheduler : Timer +{ + private readonly PriorityQueue _schedule = new(); + + public static EventScheduler Shared { get; private set; } + + public static IRecurrencePattern Hourly => new HourlyRecurrencePattern(); + + public static IRecurrencePattern Daily => new DailyRecurrencePattern(); + + // Recur every week, on the same day/time as the first occurence + public static IRecurrencePattern Weekly => new WeeklyRecurrencePattern(); + + // Recur every two weeks, on the same day/time as the first occurence + public static IRecurrencePattern Biweekly => new WeeklyRecurrencePattern(2); + + public static IRecurrencePattern Monthly => new MonthlyRecurrencePattern(); + + public static IRecurrencePattern Yearly => new MonthlyRecurrencePattern(-1, 12); + + // For each of the days of the week + private static readonly Dictionary _monthlyRecurrenceByDay = []; + + public static IRecurrencePattern GetMonthlyRecurrence(int dayOfMonth) { - void OnEventScheduled(); + ref var pattern = ref CollectionsMarshal.GetValueRefOrAddDefault(_monthlyRecurrenceByDay, dayOfMonth, out var exists); + if (!exists) + { + pattern = new MonthlyRecurrencePattern(dayOfMonth); + } + + return pattern; } - public class EventScheduleEntry + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent HourlyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Shared.ScheduleEvent(startOn, action, Hourly, timeZone); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent DailyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Shared.ScheduleEvent(startOn, action, Daily, timeZone); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent WeeklyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Shared.ScheduleEvent(startOn, action, Weekly, timeZone); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent BiweeklyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Shared.ScheduleEvent(startOn, action, Biweekly, timeZone); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent MonthlyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Shared.ScheduleEvent(startOn, action, GetMonthlyRecurrence(startOn.Day), timeZone); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent YearlyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Shared.ScheduleEvent(startOn, action, GetMonthlyRecurrence(startOn.Day), timeZone); + + public static void Configure() { - private readonly IEvent _event; - private TimeSpan _offset; - - public EventScheduleEntry(IEvent e, DateTime firstSpawn, TimeSpan interval, TimeSpan offset) - { - _offset = offset; - _event = e; - Interval = interval; - NextOccurrence = firstSpawn; - } - - public DateTime NextOccurrence { get; private set; } - public TimeSpan Interval { get; } - - public void Occur() - { - NextOccurrence += Interval; - - _event?.OnEventScheduled(); - } - - public override string ToString() => _event?.ToString(); + Shared ??= new EventScheduler(); + Shared.Start(); } - public class EventScheduler : Timer + private EventScheduler() : base(TimeSpan.Zero, TimeSpan.FromSeconds(1.0)) { - private static EventScheduler _instance; - private readonly List _schedule = new(); + } - private EventScheduler() : base(TimeSpan.Zero, TimeSpan.FromSeconds(1.0)) + public ScheduledEvent ScheduleEvent( + DateTime startOn, + Action callback, + IRecurrencePattern recurrencePattern = null, + TimeZoneInfo timeZone = null + ) => ScheduleEvent(startOn, TimeOnly.FromDateTime(startOn), callback, recurrencePattern, timeZone); + + public ScheduledEvent ScheduleEvent( + DateTime after, + TimeOnly time, + Action callback, + IRecurrencePattern recurrencePattern = null, + TimeZoneInfo timeZone = null + ) + { + var scheduledEvent = new CallbackScheduledEvent(time, callback, recurrencePattern); + scheduledEvent.Schedule(this, after, timeZone); + return scheduledEvent; + } + + public void ScheduleEvent(BaseScheduledEvent entry) + { + if (entry != null && entry.NextOccurrence < DateTime.MaxValue) { + _schedule.Enqueue(entry, entry.NextOccurrence); } + } - public static EventScheduler Instance => _instance ??= new EventScheduler(); - public static List AvailableEvents { get; } = new(); - - public static void Initialize() + public void UnscheduleEvent(BaseScheduledEvent entry) + { + if (entry != null) { - Instance.Start(); + _schedule.Remove(entry, out _, out _); } + } - public void ScheduleEvent(IEvent e, int hour, int min) + protected override void OnTick() + { + var now = Core.Now; + + while (_schedule.Count > 0) { - ScheduleEvent(e, hour, min, TimeSpan.FromDays(1.0)); - } - - public void ScheduleEvent(IEvent e, int hour, int min, TimeSpan interval) - { - var now = Core.Now; - var firstRun = new DateTime(now.Year, now.Month, now.Day, hour, min, 0); - - while (now > firstRun) + var entry = _schedule.Peek(); + var cancelled = entry.Cancelled; + if (!cancelled && entry.NextOccurrence > now) { - firstRun += interval; + break; } - ScheduleEvent( - new EventScheduleEntry(e, firstRun, interval, TimeSpan.FromHours(hour) + TimeSpan.FromMinutes(min)) - ); - } + _schedule.Dequeue(); - public void ScheduleEvent(EventScheduleEntry e) - { - _schedule.Add(e); - } - - public void RemoveEvent(EventScheduleEntry entry) - { - _schedule.Remove(entry); - } - - protected override void OnTick() - { - foreach (var entry in _schedule) + if (cancelled) { - if (entry.NextOccurrence <= Core.Now) - { - entry.Occur(); - } + continue; } + + entry.Advance(); // Advances the event and self queues if necessary } } } diff --git a/Projects/UOContent/Engines/Events/MonthDay.cs b/Projects/UOContent/Engines/Events/MonthDay.cs new file mode 100644 index 000000000..e4b4e7e34 --- /dev/null +++ b/Projects/UOContent/Engines/Events/MonthDay.cs @@ -0,0 +1,66 @@ +using System; + +namespace Server.Engines.Events; + +public record struct MonthDay : IComparable +{ + public byte Month { get; } + public byte Day { get; } + + public MonthDay(int year, int month, int day) + { + if (month is < 1 or > 12) + { + throw new ArgumentOutOfRangeException(nameof(month), "Month must be between 1 and 12."); + } + + var daysInMonth = DateTime.DaysInMonth(year, month); + + if (day < 1 || day > daysInMonth) + { + throw new ArgumentOutOfRangeException(nameof(day), $"Day must be between 1 and {daysInMonth} for month {month}."); + } + + Month = (byte)month; + Day = (byte)day; + } + + public int CompareTo(MonthDay other) + { + var monthComparison = Month.CompareTo(other.Month); + return monthComparison != 0 ? monthComparison : Day.CompareTo(other.Day); + } + + public static bool operator <(MonthDay left, MonthDay right) => left.CompareTo(right) < 0; + + public static bool operator >(MonthDay left, MonthDay right) => left.CompareTo(right) > 0; + + public static bool operator <=(MonthDay left, MonthDay right) => left.CompareTo(right) <= 0; + + public static bool operator >=(MonthDay left, MonthDay right) => left.CompareTo(right) >= 0; +} + +public static class MonthDayExtensions +{ + public static bool IsBetween(this DateTime dateTime, MonthDay start, MonthDay end) + { + var checkMonth = dateTime.Month; + var checkDay = dateTime.Day; + + var startMonth = start.Month; + var startDay = start.Day; + var endMonth = end.Month; + var endDay = end.Day; + + var isAfterStart = checkMonth > startMonth || checkMonth == startMonth && checkDay >= startDay; + var isBeforeEnd = checkMonth < endMonth || checkMonth == endMonth && checkDay <= endDay; + + if (startMonth < endMonth || startMonth == endMonth && startDay <= endDay) + { + return isAfterStart && isBeforeEnd; + } + + // Complex case: spans year boundary (e.g., Nov 15 - Feb 15) + return isAfterStart || isBeforeEnd; + } +} diff --git a/Projects/UOContent/Engines/Events/ScheduledEvent.cs b/Projects/UOContent/Engines/Events/ScheduledEvent.cs new file mode 100644 index 000000000..3f90227cb --- /dev/null +++ b/Projects/UOContent/Engines/Events/ScheduledEvent.cs @@ -0,0 +1,32 @@ +using System; + +namespace Server.Engines.Events; + +public abstract class ScheduledEvent : BaseScheduledEvent +{ + public IRecurrencePattern Recurrence { get; } + public TimeOnly Time { get; } + public DateTime EndDate { get; } + + public ScheduledEvent(TimeOnly time, IRecurrencePattern recurrence = null) + : this(time, DateTime.MaxValue, recurrence) + { + } + + public ScheduledEvent( + TimeOnly time, + DateTime endOn, + IRecurrencePattern recurrence = null + ) + { + Time = time; + Recurrence = recurrence; + EndDate = endOn == DateTime.MaxValue || endOn.Kind == DateTimeKind.Utc ? endOn : endOn.LocalToUtc(TimeZone); + } + + protected override DateTime GetNextOccurrence(DateTime after) + { + var next = Recurrence?.GetNextOccurrence(after, Time, TimeZone) ?? DateTime.MaxValue; + return next >= EndDate ? DateTime.MaxValue : next; + } +} diff --git a/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs b/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs new file mode 100644 index 000000000..f4ca34f65 --- /dev/null +++ b/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs @@ -0,0 +1,29 @@ +using System; + +namespace Server.Engines.Events; + +public class YearlyCallbackScheduledEvent : YearlyScheduledEvent +{ + private readonly Action _callback; + + protected YearlyCallbackScheduledEvent( + TimeOnly time, + MonthDay yearlyStart, + MonthDay yearlyEnd, + Action callback, + IRecurrencePattern recurrence + ) : this(time, yearlyStart, yearlyEnd, DateTime.MaxValue, callback, recurrence) + { + } + + protected YearlyCallbackScheduledEvent( + TimeOnly time, + MonthDay yearlyStart, + MonthDay yearlyEnd, + DateTime endOn, + Action callback, + IRecurrencePattern recurrence + ) : base(time, yearlyStart, yearlyEnd, endOn, recurrence) => _callback = callback; + + public override void OnEvent() => _callback(); +} diff --git a/Projects/UOContent/Engines/Events/YearlyScheduledEvent.cs b/Projects/UOContent/Engines/Events/YearlyScheduledEvent.cs new file mode 100644 index 000000000..0deceaca8 --- /dev/null +++ b/Projects/UOContent/Engines/Events/YearlyScheduledEvent.cs @@ -0,0 +1,67 @@ +using System; + +namespace Server.Engines.Events; + +public abstract class YearlyScheduledEvent : ScheduledEvent +{ + public MonthDay YearlyStart { get; } + + public MonthDay YearlyEnd { get; } + + protected YearlyScheduledEvent( + TimeOnly time, + MonthDay yearlyStart, + MonthDay yearlyEnd, + IRecurrencePattern recurrence + ) : this( time, yearlyStart, yearlyEnd, DateTime.MaxValue, recurrence) + { + } + + protected YearlyScheduledEvent( + TimeOnly time, + MonthDay yearlyStart, + MonthDay yearlyEnd, + DateTime endOn, + IRecurrencePattern recurrence + ) : base(time, endOn, recurrence) + { + YearlyStart = yearlyStart; + YearlyEnd = yearlyEnd; + } + + protected override DateTime GetNextOccurrence(DateTime after) + { + var next = base.GetNextOccurrence(after); + + if (next == DateTime.MaxValue) + { + return DateTime.MaxValue; + } + + var localNext = TimeZoneInfo.ConvertTimeFromUtc(next, TimeZone); + + if (localNext.IsBetween(YearlyStart, YearlyEnd)) + { + return next; + } + + int yearToUse; + + // If we're after the end of this year's range but before the start of next year's range + if (YearlyStart > YearlyEnd && localNext.Month > YearlyEnd.Month) + { + // We're in the same calendar year, targeting this year's start + yearToUse = localNext.Year; + } + else + { + // Either we're in a non-spanning range, or we're in the early part of next year + // In either case, we need to advance to the next year's start + yearToUse = localNext.Year + 1; + } + + var nextYearStartUtc = new DateTime(yearToUse, YearlyStart.Month, YearlyStart.Day).LocalToUtc(TimeZone); + + return Recurrence!.GetNextOccurrence(nextYearStartUtc, Time, TimeZone); + } +} diff --git a/Projects/UOContent/Engines/Factions/Gumps/SheriffGump.cs b/Projects/UOContent/Engines/Factions/Gumps/SheriffGump.cs index f5a466259..112b6dc4b 100644 --- a/Projects/UOContent/Engines/Factions/Gumps/SheriffGump.cs +++ b/Projects/UOContent/Engines/Factions/Gumps/SheriffGump.cs @@ -118,7 +118,7 @@ public class SheriffGump : FactionGump private void CenterItem(int itemID, int x, int y, int w, int h) { - var rc = ItemBounds.Table[itemID]; + var rc = ItemBounds.Bounds[itemID]; AddItem(x + (w - rc.Width) / 2 - rc.X, y + (h - rc.Height) / 2 - rc.Y, itemID); } diff --git a/Projects/UOContent/Engines/Help/PagePromptGump.cs b/Projects/UOContent/Engines/Help/PagePromptGump.cs index 7814ffedd..a03612be4 100644 --- a/Projects/UOContent/Engines/Help/PagePromptGump.cs +++ b/Projects/UOContent/Engines/Help/PagePromptGump.cs @@ -30,7 +30,7 @@ public sealed class PagePromptGump : StaticGump builder.AddTextEntry(120, 168, 400, 200, 1153, 0, ""); builder.AddButton(175, 355, 2074, 2075, 1); // Okay - builder. AddButton(405, 355, 2073, 2072, 0); // Cancel + builder.AddButton(405, 355, 2073, 2072, 0); // Cancel } public override void OnResponse(NetState sender, in RelayInfo info) diff --git a/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs b/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs index 695e912d6..1f9ac288d 100644 --- a/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs +++ b/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs @@ -837,8 +837,8 @@ namespace Server.Items protected override void BuildStrings(ref GumpStringsBuilder builder) { - builder.SetStringSlot("cylinders", _correctCylinders.ToString()); - builder.SetStringSlot("colors", _correctColors.ToString()); + builder.SetStringSlot("cylinders", $"{_correctCylinders}"); + builder.SetStringSlot("colors", $"{_correctColors}"); } } } diff --git a/Projects/UOContent/Engines/ML Quests/Definitions/Heartwood.cs b/Projects/UOContent/Engines/ML Quests/Definitions/Heartwood.cs index 2ac1e889f..4bf21fdf0 100644 --- a/Projects/UOContent/Engines/ML Quests/Definitions/Heartwood.cs +++ b/Projects/UOContent/Engines/ML Quests/Definitions/Heartwood.cs @@ -2158,9 +2158,10 @@ public partial class Landy : BaseCreature AddItem(new ShortPants(Utility.RandomYellowHue())); AddItem(new Tunic(Utility.RandomYellowHue())); - Item gloves = new LeafGloves(); - gloves.Hue = Utility.RandomYellowHue(); - AddItem(gloves); + AddItem(new LeafGloves + { + Hue = Utility.RandomYellowHue() + }); } public override bool IsInvulnerable => true; @@ -2720,11 +2721,10 @@ public partial class Tholef : BaseCreature AddItem(new ShortPants(0x28C)); AddItem(new Shirt(0x28C)); - Item item; - - item = new LeafArms(); - item.Hue = 0x28C; - AddItem(item); + AddItem(new LeafArms + { + Hue = 0x28C + }); } public override bool IsInvulnerable => true; @@ -2807,11 +2807,10 @@ public partial class Waelian : BaseCreature AddItem(new LongPants(0x340)); AddItem(new GemmedCirclet()); - Item item; - - item = new LeafChest(); - item.Hue = 0x344; - AddItem(item); + AddItem(new LeafChest + { + Hue = 0x344 + }); } public override bool IsInvulnerable => true; @@ -3057,11 +3056,10 @@ public partial class Lohn : BaseCreature AddItem(new SmithHammer()); AddItem(new GemmedCirclet()); - Item item; - - item = new LeafChest(); - item.Hue = 0x359; - AddItem(item); + AddItem(new LeafChest + { + Hue = 0x359 + }); } public override bool IsInvulnerable => true; @@ -3405,11 +3403,10 @@ public partial class ElderVicaie : BaseCreature AddItem(new ElvenBoots()); AddItem(new Tunic(0x732)); - Item item; - - item = new LeafLegs(); - item.Hue = 0x3B2; - AddItem(item); + AddItem(new LeafLegs + { + Hue = 0x3B2 + }); } public override bool IsInvulnerable => true; @@ -3573,11 +3570,10 @@ public partial class Bolaevin : BaseCreature AddItem(new LeafChest()); AddItem(new LeafArms()); - Item item; - - item = new LeafLegs(); - item.Hue = 0x1BB; - AddItem(item); + AddItem(new LeafLegs + { + Hue = 0x1BB + }); } public override bool IsInvulnerable => true; diff --git a/Projects/UOContent/Engines/ML Quests/Definitions/NewHavenSkillTraining.cs b/Projects/UOContent/Engines/ML Quests/Definitions/NewHavenSkillTraining.cs index fd0401b94..67781a6d5 100644 --- a/Projects/UOContent/Engines/ML Quests/Definitions/NewHavenSkillTraining.cs +++ b/Projects/UOContent/Engines/ML Quests/Definitions/NewHavenSkillTraining.cs @@ -1274,11 +1274,10 @@ public partial class Mithneral : BaseCreature AddItem(new Backpack()); AddItem(new Sandals()); - Item item; - - item = new GustarShroud(); - item.Hue = 0x51C; - AddItem(item); + AddItem(new GustarShroud + { + Hue = 0x51C + }); } public override bool IsInvulnerable => true; @@ -1876,10 +1875,9 @@ public partial class GeorgeHephaestus : Blacksmith AddItem(new Bascinet()); AddItem(new FullApron(0x8AB)); - Item item; - - item = new SmithHammer(); - item.Hue = 0x8AB; - AddItem(item); + AddItem(new SmithHammer + { + Hue = 0x8AB + }); } } diff --git a/Projects/UOContent/Engines/ML Quests/Definitions/NewHavenTraining.cs b/Projects/UOContent/Engines/ML Quests/Definitions/NewHavenTraining.cs index 67cf71bb1..324898047 100644 --- a/Projects/UOContent/Engines/ML Quests/Definitions/NewHavenTraining.cs +++ b/Projects/UOContent/Engines/ML Quests/Definitions/NewHavenTraining.cs @@ -472,11 +472,10 @@ public partial class Gervis : BaseCreature AddItem(new Doublet(0x652)); AddItem(new SmithHammer()); - Item item; - - item = new LeatherGloves(); - item.Hue = 0x3B2; - AddItem(item); + AddItem(new LeatherGloves + { + Hue = 0x3B2 + }); } public override bool IsInvulnerable => true; @@ -750,11 +749,10 @@ public partial class Hargrove : BaseCreature AddItem(new Bandana(0x20)); AddItem(new BattleAxe()); - Item item; - - item = new PlateGloves(); - item.Hue = 0x21E; - AddItem(item); + AddItem(new PlateGloves + { + Hue = 0x21E + }); } public override bool IsInvulnerable => true; diff --git a/Projects/UOContent/Engines/ML Quests/Definitions/Sanctuary.cs b/Projects/UOContent/Engines/ML Quests/Definitions/Sanctuary.cs index 1e03fc986..605fca1c6 100644 --- a/Projects/UOContent/Engines/ML Quests/Definitions/Sanctuary.cs +++ b/Projects/UOContent/Engines/ML Quests/Definitions/Sanctuary.cs @@ -784,11 +784,10 @@ public partial class Beotham : BaseCreature AddItem(new ShortPants(0x522)); AddItem(new FancyShirt(0x515)); - Item item; - - item = new LeafGloves(); - item.Hue = 0x901; - AddItem(item); + AddItem(new LeafGloves + { + Hue = 0x901 + }); } public override bool IsInvulnerable => true; @@ -1014,11 +1013,10 @@ public partial class LorekeeperRollarn : BaseCreature AddItem(new Circlet()); AddItem(new LeafChest()); - Item item; - - item = new LeafLegs(); - item.Hue = 0x71A; - AddItem(item); + AddItem(new LeafLegs + { + Hue = 0x71A + }); } public override bool IsInvulnerable => true; diff --git a/Projects/UOContent/Engines/ML Quests/Definitions/Spellweaving.cs b/Projects/UOContent/Engines/ML Quests/Definitions/Spellweaving.cs index fabd58935..96c9431ea 100644 --- a/Projects/UOContent/Engines/ML Quests/Definitions/Spellweaving.cs +++ b/Projects/UOContent/Engines/ML Quests/Definitions/Spellweaving.cs @@ -599,9 +599,10 @@ public partial class Synaeva : BaseCreature SetSkill(SkillName.Meditation, 60.0, 80.0); SetSkill(SkillName.Focus, 60.0, 80.0); - Item item = new RavenHelm(); - item.Hue = Utility.RandomGreenHue(); - AddItem(item); + AddItem(new RavenHelm + { + Hue = Utility.RandomGreenHue() + }); AddItem(new FemaleLeafChest()); AddItem(new LeafArms()); diff --git a/Projects/UOContent/Engines/Plants/MiscItems/GreenThorns.cs b/Projects/UOContent/Engines/Plants/MiscItems/GreenThorns.cs index e3db73227..846f8d1dd 100644 --- a/Projects/UOContent/Engines/Plants/MiscItems/GreenThorns.cs +++ b/Projects/UOContent/Engines/Plants/MiscItems/GreenThorns.cs @@ -509,7 +509,7 @@ public class FurrowsGreenThornsEffect : GreenThornsEffect // * A magical bunny leaps out of its hole, disturbed by the thorn's effect! * dummy.PublicOverheadMessage(MessageType.Regular, 0x3B2, 1114428); - BaseCreature spawn = new VorpalBunny(); + var spawn = new VorpalBunny(); if (!SpawnCreature(spawn)) { spawn.Delete(); @@ -564,7 +564,7 @@ public class SwampGreenThornsEffect : GreenThornsEffect dummy.PublicOverheadMessage(MessageType.Regular, 0x3B2, 1114429); Effects.PlaySound(Location, Map, 0x2B0); - BaseCreature spawn = new WhippingVine(); + var spawn = new WhippingVine(); if (!SpawnCreature(spawn)) { spawn.Delete(); @@ -618,7 +618,7 @@ public class SnowGreenThornsEffect : GreenThornsEffect // * Slithering ice serpents rise to the surface to investigate the disturbance! * dummy.PublicOverheadMessage(MessageType.Regular, 0x3B2, 1114430); - BaseCreature spawn = new GiantIceWorm(); + var spawn = new GiantIceWorm(); if (!SpawnCreature(spawn)) { spawn.Delete(); @@ -626,7 +626,7 @@ public class SnowGreenThornsEffect : GreenThornsEffect for (var i = 0; i < 3; i++) { - BaseCreature snake = new IceSnake(); + var snake = new IceSnake(); if (!SpawnCreature(snake)) { snake.Delete(); diff --git a/Projects/UOContent/Engines/Plants/PlantItem.cs b/Projects/UOContent/Engines/Plants/PlantItem.cs index 13c28cdd9..53266f2b4 100644 --- a/Projects/UOContent/Engines/Plants/PlantItem.cs +++ b/Projects/UOContent/Engines/Plants/PlantItem.cs @@ -48,7 +48,7 @@ public partial class PlantItem : Item, ISecurable private PlantSystem _plantSystem; [SerializableFieldSaveFlag(5)] - private bool ShouldSerializePlantSystem() => _plantStatus != PlantStatus.DecorativePlant; + private bool ShouldSerializePlantSystem() => _plantStatus < PlantStatus.DecorativePlant; // For clients older than 7.0.12.0 private ObjectPropertyList _oldClientPropertyList; diff --git a/Projects/UOContent/Engines/Quests/Ambitious Solen Queen/AmbitiousQueenQuest.cs b/Projects/UOContent/Engines/Quests/Ambitious Solen Queen/AmbitiousQueenQuest.cs index fff90c899..095dc75f1 100644 --- a/Projects/UOContent/Engines/Quests/Ambitious Solen Queen/AmbitiousQueenQuest.cs +++ b/Projects/UOContent/Engines/Quests/Ambitious Solen Queen/AmbitiousQueenQuest.cs @@ -70,7 +70,7 @@ namespace Server.Engines.Quests.Ambitious { if (bagOfSending) { - Item reward = new BagOfSending(); + var reward = new BagOfSending(); if (player.PlaceInBackpack(reward)) { @@ -85,7 +85,7 @@ namespace Server.Engines.Quests.Ambitious if (powderOfTranslocation) { - Item reward = new PowderOfTranslocation(Utility.RandomMinMax(10, 12)); + var reward = new PowderOfTranslocation(Utility.RandomMinMax(10, 12)); if (player.PlaceInBackpack(reward)) { @@ -100,7 +100,7 @@ namespace Server.Engines.Quests.Ambitious if (gold) { - Item reward = new Gold(Utility.RandomMinMax(250, 350)); + var reward = new Gold(Utility.RandomMinMax(250, 350)); if (player.PlaceInBackpack(reward)) { diff --git a/Projects/UOContent/Engines/Quests/Dark Tides/Items/KronusScrollBox.cs b/Projects/UOContent/Engines/Quests/Dark Tides/Items/KronusScrollBox.cs index 4cd34c687..8451bc1b5 100644 --- a/Projects/UOContent/Engines/Quests/Dark Tides/Items/KronusScrollBox.cs +++ b/Projects/UOContent/Engines/Quests/Dark Tides/Items/KronusScrollBox.cs @@ -33,7 +33,7 @@ public partial class KronusScrollBox : MetalBox if (obj?.Completed == false || DarkTidesQuest.HasLostCallingScroll(from)) { - Item scroll = new KronusScroll(); + var scroll = new KronusScroll(); if (pm.PlaceInBackpack(scroll)) { diff --git a/Projects/UOContent/Engines/Quests/Dark Tides/Mobiles/Mardoth.cs b/Projects/UOContent/Engines/Quests/Dark Tides/Mobiles/Mardoth.cs index d5086fbd2..76eb59361 100644 --- a/Projects/UOContent/Engines/Quests/Dark Tides/Mobiles/Mardoth.cs +++ b/Projects/UOContent/Engines/Quests/Dark Tides/Mobiles/Mardoth.cs @@ -64,13 +64,15 @@ public partial class Mardoth : BaseQuester HairItemID = 0x203C; HairHue = 0x482; - Item gloves = new BoneGloves(); - gloves.Hue = 0x66D; - AddItem(gloves); + AddItem(new BoneGloves + { + Hue = 0x66D + }); - Item gorget = new PlateGorget(); - gorget.Hue = 0x1; - AddItem(gorget); + AddItem(new PlateGorget + { + Hue = 0x1 + }); } public override int GetAutoTalkRange(PlayerMobile m) => 3; diff --git a/Projects/UOContent/Engines/Quests/Emino's Undertaking/Items/EminosKatanaChest.cs b/Projects/UOContent/Engines/Quests/Emino's Undertaking/Items/EminosKatanaChest.cs index 31d895834..b6df0126e 100644 --- a/Projects/UOContent/Engines/Quests/Emino's Undertaking/Items/EminosKatanaChest.cs +++ b/Projects/UOContent/Engines/Quests/Emino's Undertaking/Items/EminosKatanaChest.cs @@ -49,7 +49,7 @@ public partial class EminosKatanaChest : WoodenChest { if (EminosUndertakingQuest.HasLostEminosKatana(from)) { - Item katana = new EminosKatana(); + var katana = new EminosKatana(); if (!player.PlaceInBackpack(katana)) { @@ -64,7 +64,7 @@ public partial class EminosKatanaChest : WoodenChest if (obj?.Completed == false) { - Item katana = new EminosKatana(); + var katana = new EminosKatana(); if (player.PlaceInBackpack(katana)) { diff --git a/Projects/UOContent/Engines/Quests/Emino's Undertaking/Mobiles/Emino.cs b/Projects/UOContent/Engines/Quests/Emino's Undertaking/Mobiles/Emino.cs index 2e4826d87..bb4e5d464 100644 --- a/Projects/UOContent/Engines/Quests/Emino's Undertaking/Mobiles/Emino.cs +++ b/Projects/UOContent/Engines/Quests/Emino's Undertaking/Mobiles/Emino.cs @@ -58,7 +58,7 @@ public partial class Emino : BaseQuester if (EminosUndertakingQuest.HasLostNoteForZoel(player)) { - Item note = new NoteForZoel(); + var note = new NoteForZoel(); if (player.PlaceInBackpack(note)) { diff --git a/Projects/UOContent/Engines/Quests/Haochi's Trials/Items/HaochisKatanaGenerator.cs b/Projects/UOContent/Engines/Quests/Haochi's Trials/Items/HaochisKatanaGenerator.cs index 991c8ff3a..5ac52c164 100644 --- a/Projects/UOContent/Engines/Quests/Haochi's Trials/Items/HaochisKatanaGenerator.cs +++ b/Projects/UOContent/Engines/Quests/Haochi's Trials/Items/HaochisKatanaGenerator.cs @@ -31,7 +31,7 @@ public partial class HaochisKatanaGenerator : Item if (HaochisTrialsQuest.HasLostHaochisKatana(player)) { - Item katana = new HaochisKatana(); + var katana = new HaochisKatana(); if (!player.PlaceInBackpack(katana)) { @@ -46,7 +46,7 @@ public partial class HaochisKatanaGenerator : Item if (obj?.Completed == false) { - Item katana = new HaochisKatana(); + var katana = new HaochisKatana(); if (player.PlaceInBackpack(katana)) { diff --git a/Projects/UOContent/Engines/Quests/The Summoning/Mobiles/Chyloth.cs b/Projects/UOContent/Engines/Quests/The Summoning/Mobiles/Chyloth.cs index 9a333909f..b70cbe9a8 100644 --- a/Projects/UOContent/Engines/Quests/The Summoning/Mobiles/Chyloth.cs +++ b/Projects/UOContent/Engines/Quests/The Summoning/Mobiles/Chyloth.cs @@ -220,7 +220,7 @@ public partial class Chyloth : BaseQuester AngryAt = null; } - member.SendGump(new ChylothPartyGump(from, member)); + member.SendGump(new ChylothPartyGump(from)); } } @@ -244,82 +244,89 @@ public partial class Chyloth : BaseQuester } } -public class ChylothPartyGump : Gump +public class ChylothPartyGump : StaticGump { private readonly Mobile _leader; - private readonly Mobile _member; - public ChylothPartyGump(Mobile leader, Mobile member) : base(150, 50) + public override bool Singleton => true; + + public ChylothPartyGump(Mobile leader) : base(150, 50) => _leader = leader; + + protected override void BuildLayout(ref StaticGumpBuilder builder) { - _leader = leader; - _member = member; + builder.SetNoClose(); - Closable = false; + builder.AddPage(); - AddPage(0); + builder.AddImage(0, 0, 3600); - AddImage(0, 0, 3600); + builder.AddImageTiled(0, 14, 15, 200, 3603); + builder.AddImageTiled(380, 14, 14, 200, 3605); + builder.AddImage(0, 201, 3606); + builder.AddImageTiled(15, 201, 370, 16, 3607); + builder.AddImageTiled(15, 0, 370, 16, 3601); + builder.AddImage(380, 0, 3602); + builder.AddImage(380, 201, 3608); + builder.AddImageTiled(15, 15, 365, 190, 2624); - AddImageTiled(0, 14, 15, 200, 3603); - AddImageTiled(380, 14, 14, 200, 3605); - AddImage(0, 201, 3606); - AddImageTiled(15, 201, 370, 16, 3607); - AddImageTiled(15, 0, 370, 16, 3601); - AddImage(380, 0, 3602); - AddImage(380, 201, 3608); - AddImageTiled(15, 15, 365, 190, 2624); + builder.AddRadio(30, 140, 9727, 9730, true, 1); + builder.AddHtmlLocalized(65, 145, 300, 25, 1050050, 0x7FFF); // Yes, let's go! - AddRadio(30, 140, 9727, 9730, true, 1); - AddHtmlLocalized(65, 145, 300, 25, 1050050, 0x7FFF); // Yes, let's go! - - AddRadio(30, 175, 9727, 9730, false, 0); - AddHtmlLocalized(65, 178, 300, 25, 1050049, 0x7FFF); // No thanks, I'd rather stay here. + builder.AddRadio(30, 175, 9727, 9730, false, 0); + builder.AddHtmlLocalized(65, 178, 300, 25, 1050049, 0x7FFF); // No thanks, I'd rather stay here. // Another player has paid Chyloth for your passage across lake Mortis: - AddHtmlLocalized(30, 20, 360, 35, 1050047, 0x7FFF); + builder.AddHtmlLocalized(30, 20, 360, 35, 1050047, 0x7FFF); - AddHtmlLocalized(30, 105, 345, 40, 1050048, 0x5B2D); // Do you wish to accept their invitation at this time? + builder.AddHtmlLocalized(30, 105, 345, 40, 1050048, 0x5B2D); // Do you wish to accept their invitation at this time? - AddImage(65, 72, 5605); + builder.AddImage(65, 72, 5605); - AddImageTiled(80, 90, 200, 1, 9107); - AddImageTiled(95, 92, 200, 1, 9157); + builder.AddImageTiled(80, 90, 200, 1, 9107); + builder.AddImageTiled(95, 92, 200, 1, 9157); - AddLabel(90, 70, 1645, leader.Name); + builder.AddLabelPlaceholder(90, 70, 1645, "leader"); - AddButton(290, 175, 247, 248, 2); + builder.AddButton(290, 175, 247, 248, 2); - AddImageTiled(15, 14, 365, 1, 9107); - AddImageTiled(380, 14, 1, 190, 9105); - AddImageTiled(15, 205, 365, 1, 9107); - AddImageTiled(15, 14, 1, 190, 9105); - AddImageTiled(0, 0, 395, 1, 9157); - AddImageTiled(394, 0, 1, 217, 9155); - AddImageTiled(0, 216, 395, 1, 9157); - AddImageTiled(0, 0, 1, 217, 9155); + builder.AddImageTiled(15, 14, 365, 1, 9107); + builder.AddImageTiled(380, 14, 1, 190, 9105); + builder.AddImageTiled(15, 205, 365, 1, 9107); + builder.AddImageTiled(15, 14, 1, 190, 9105); + builder.AddImageTiled(0, 0, 395, 1, 9157); + builder.AddImageTiled(394, 0, 1, 217, 9155); + builder.AddImageTiled(0, 216, 395, 1, 9157); + builder.AddImageTiled(0, 0, 1, 217, 9155); + } + + protected override void BuildStrings(ref GumpStringsBuilder builder) + { + builder.SetStringSlot("leader", _leader.Name); } public override void OnResponse(NetState sender, in RelayInfo info) { + var member = sender.Mobile; + if (info.ButtonID == 2 && info.IsSwitched(1)) { - if (_member.Region.IsPartOf("Doom")) + if (member.Region.IsPartOf("Doom")) { // ~1_NAME~ has accepted your invitation to cross lake Mortis. - _leader.SendLocalizedMessage(1050054, _member.Name); + _leader.SendLocalizedMessage(1050054, member.Name); - Chyloth.TeleportToFerry(_member); + Chyloth.TeleportToFerry(member); } else { - _member.SendLocalizedMessage(1050051); // The invitation has been revoked. + member.SendLocalizedMessage(1050051); // The invitation has been revoked. } } else { - _member.SendLocalizedMessage(1050052); // You have declined their invitation. + member.SendLocalizedMessage(1050052); // You have declined their invitation. // ~1_NAME~ has declined your invitation to cross lake Mortis. - _leader.SendLocalizedMessage(1050053, _member.Name); + _leader.SendLocalizedMessage(1050053, member.Name); } } } diff --git a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/DaemonBloodChest.cs b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/DaemonBloodChest.cs index 256cdcb80..a9abd82de 100644 --- a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/DaemonBloodChest.cs +++ b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/DaemonBloodChest.cs @@ -34,7 +34,7 @@ public partial class DaemonBloodChest : MetalChest return; } - Item vial = new QuestDaemonBlood(); + var vial = new QuestDaemonBlood(); if (player.PlaceInBackpack(vial)) { diff --git a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/SchmendrickApprenticeCorpse.cs b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/SchmendrickApprenticeCorpse.cs index 33793a202..20b213646 100644 --- a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/SchmendrickApprenticeCorpse.cs +++ b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/SchmendrickApprenticeCorpse.cs @@ -27,6 +27,8 @@ public partial class SchmendrickApprenticeCorpse : Corpse _lantern = new Lantern { Movable = false, Protected = true }; _lantern.Ignite(); + + Owner = null; } private static Mobile GetOwner() @@ -152,5 +154,7 @@ public partial class SchmendrickApprenticeCorpse : Corpse { _lantern.Delete(); } + + Owner?.Delete(); } } diff --git a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Mobiles/Dryad.cs b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Mobiles/Dryad.cs index 108687c8b..8831e9a9a 100644 --- a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Mobiles/Dryad.cs +++ b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Mobiles/Dryad.cs @@ -78,7 +78,7 @@ public partial class Dryad : BaseQuester { FocusTo(player); - Item fertileDirt = new QuestFertileDirt(); + var fertileDirt = new QuestFertileDirt(); if (!player.PlaceInBackpack(fertileDirt)) { @@ -111,7 +111,7 @@ public partial class Dryad : BaseQuester FocusTo(from); - Item fertileDirt = new QuestFertileDirt(); + var fertileDirt = new QuestFertileDirt(); if (!player.PlaceInBackpack(fertileDirt)) { diff --git a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Objectives.cs b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Objectives.cs index e69e4c14a..53be36108 100644 --- a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Objectives.cs +++ b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Objectives.cs @@ -328,7 +328,7 @@ namespace Server.Engines.Quests.Haven 1049330 ); // You have been ambushed! Fight for your honor!!! - BaseCreature creature = new HordeMinion(); + var creature = new HordeMinion(); creature.MoveToWorld(new Point3D(x, y, z), Map.Trammel); creature.Combatant = player; } diff --git a/Projects/UOContent/Engines/Quests/Witch Apprentice/Objectives.cs b/Projects/UOContent/Engines/Quests/Witch Apprentice/Objectives.cs index 723a129f0..2fb4395bb 100644 --- a/Projects/UOContent/Engines/Quests/Witch Apprentice/Objectives.cs +++ b/Projects/UOContent/Engines/Quests/Witch Apprentice/Objectives.cs @@ -52,7 +52,7 @@ namespace Server.Engines.Quests.Hag Effects.SendLocationEffect(m_CorpseLocation, map, 0x3728, 10); Effects.PlaySound(m_CorpseLocation, map, 0x1FE); - Mobile imp = new Zeefzorpul(); + var imp = new Zeefzorpul(); imp.MoveToWorld(m_CorpseLocation, map); // * You see a strange imp stealing a scrap of paper from the bloodied corpse * @@ -216,7 +216,7 @@ namespace Server.Engines.Quests.Hag Effects.SendLocationEffect(ImpLocation, map, 0x3728, 10); Effects.PlaySound(ImpLocation, map, 0x1FE); - Mobile imp = new Zeefzorpul(); + var imp = new Zeefzorpul(); imp.MoveToWorld(ImpLocation, map); imp.Direction = imp.GetDirectionTo(from); diff --git a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs index 28e423313..258d2956b 100644 --- a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs +++ b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs @@ -85,7 +85,7 @@ public abstract partial class BaseSpawner : Item, ISpawner public BaseSpawner( int amount, int minDelay, int maxDelay, int team, int homeRange, - params string[] spawnedNames + params ReadOnlySpan spawnedNames ) : this( amount, TimeSpan.FromMinutes(minDelay), @@ -99,7 +99,7 @@ public abstract partial class BaseSpawner : Item, ISpawner public BaseSpawner( int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange, - params string[] spawnedNames + params ReadOnlySpan spawnedNames ) : base(0x1f13) { _guid = Guid.NewGuid(); diff --git a/Projects/UOContent/Engines/Spawners/ProximitySpawner.cs b/Projects/UOContent/Engines/Spawners/ProximitySpawner.cs index ad1dde122..ad03c2ad8 100644 --- a/Projects/UOContent/Engines/Spawners/ProximitySpawner.cs +++ b/Projects/UOContent/Engines/Spawners/ProximitySpawner.cs @@ -66,7 +66,7 @@ public partial class ProximitySpawner : Spawner [Constructible(AccessLevel.Developer)] public ProximitySpawner( int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange, - params string[] spawnedNames + params ReadOnlySpan spawnedNames ) : base(amount, minDelay, maxDelay, team, homeRange, spawnedNames) { } @@ -74,7 +74,7 @@ public partial class ProximitySpawner : Spawner [Constructible(AccessLevel.Developer)] public ProximitySpawner( int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange, int triggerRange, - TextDefinition spawnMessage, bool instantFlag, params string[] spawnedNames + TextDefinition spawnMessage, bool instantFlag, params ReadOnlySpan spawnedNames ) : base(amount, minDelay, maxDelay, team, homeRange, spawnedNames) { TriggerRange = triggerRange; diff --git a/Projects/UOContent/Engines/Spawners/RegionSpawner.cs b/Projects/UOContent/Engines/Spawners/RegionSpawner.cs index 90deae17f..f4f889d34 100644 --- a/Projects/UOContent/Engines/Spawners/RegionSpawner.cs +++ b/Projects/UOContent/Engines/Spawners/RegionSpawner.cs @@ -42,7 +42,7 @@ public partial class RegionSpawner : Spawner [Constructible(AccessLevel.Developer)] public RegionSpawner( int amount, int minDelay, int maxDelay, int team, int homeRange, - params string[] spawnedNames + params ReadOnlySpan spawnedNames ) : this( amount, TimeSpan.FromMinutes(minDelay), @@ -57,7 +57,7 @@ public partial class RegionSpawner : Spawner [Constructible(AccessLevel.Developer)] public RegionSpawner( int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange, - params string[] spawnedNames + params ReadOnlySpan spawnedNames ) : base(amount, minDelay, maxDelay, team, homeRange, spawnedNames) { } diff --git a/Projects/UOContent/Engines/Spawners/Spawner.cs b/Projects/UOContent/Engines/Spawners/Spawner.cs index 5a5b1d1f2..49a3539ae 100644 --- a/Projects/UOContent/Engines/Spawners/Spawner.cs +++ b/Projects/UOContent/Engines/Spawners/Spawner.cs @@ -21,7 +21,7 @@ public partial class Spawner : BaseSpawner [Constructible(AccessLevel.Developer)] public Spawner( int amount, int minDelay, int maxDelay, int team, int homeRange, - params string[] spawnedNames + params ReadOnlySpan spawnedNames ) : this( amount, TimeSpan.FromMinutes(minDelay), @@ -36,7 +36,7 @@ public partial class Spawner : BaseSpawner [Constructible(AccessLevel.Developer)] public Spawner( int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange, - params string[] spawnedNames + params ReadOnlySpan spawnedNames ) : base(amount, minDelay, maxDelay, team, homeRange, spawnedNames) { } diff --git a/Projects/UOContent/Engines/Treasures of Tokuno/TreasuresOfTokuno.cs b/Projects/UOContent/Engines/Treasures of Tokuno/TreasuresOfTokuno.cs index 353016c07..0b105e7b6 100644 --- a/Projects/UOContent/Engines/Treasures of Tokuno/TreasuresOfTokuno.cs +++ b/Projects/UOContent/Engines/Treasures of Tokuno/TreasuresOfTokuno.cs @@ -279,10 +279,10 @@ namespace Server.Mobiles AddItem(new Backpack()); AddItem(new Kamishimo(0x483)); - Item item = new LightPlateJingasa(); - item.Hue = 0x711; - - AddItem(item); + AddItem(new LightPlateJingasa + { + Hue = 0x711 + }); } public override bool CanBeDamaged() => false; diff --git a/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/Gumps/CharacterStatueGump.cs b/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/Gumps/CharacterStatueGump.cs index 87bc2d088..319214463 100644 --- a/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/Gumps/CharacterStatueGump.cs +++ b/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/Gumps/CharacterStatueGump.cs @@ -68,7 +68,7 @@ namespace Server.Gumps } } - private int GetMaterialNumber(StatueType type, StatueMaterial material) + private static int GetMaterialNumber(StatueType type, StatueMaterial material) { switch (material) { @@ -96,7 +96,7 @@ namespace Server.Gumps } } - private int GetDirectionNumber(Direction direction) + private static int GetDirectionNumber(Direction direction) { return direction switch { diff --git a/Projects/UOContent/Engines/Virtues/VirtueSystem.cs b/Projects/UOContent/Engines/Virtues/VirtueSystem.cs index b42020703..737770dc0 100644 --- a/Projects/UOContent/Engines/Virtues/VirtueSystem.cs +++ b/Projects/UOContent/Engines/Virtues/VirtueSystem.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; +using ModernUO.CodeGeneratedEvents; using Server.Collections; using Server.Logging; using Server.Mobiles; @@ -75,6 +76,9 @@ public class VirtueSystem : GenericPersistence } } + [OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))] + public static void OnPlayerDeleted(PlayerMobile pm) => _playerVirtues.Remove(pm); + public override void Serialize(IGenericWriter writer) { writer.WriteEncodedInt(0); // version @@ -98,7 +102,7 @@ public class VirtueSystem : GenericPersistence var virtues = new VirtueContext(); virtues.Deserialize(reader); - if (virtues.IsUsed()) + if (player != null && virtues.IsUsed()) { _playerVirtues.Add(player, virtues); } diff --git a/Projects/UOContent/Gumps/AdminGump.cs b/Projects/UOContent/Gumps/AdminGump.cs index 816d1967b..fa22ceca6 100644 --- a/Projects/UOContent/Gumps/AdminGump.cs +++ b/Projects/UOContent/Gumps/AdminGump.cs @@ -170,7 +170,7 @@ namespace Server.Gumps AddLabel(150, 150, LabelHue, banned.ToString()); AddLabel(20, 170, LabelHue, "Firewalled:"); - AddLabel(150, 170, LabelHue, Firewall.FirewallSet.Count.ToString()); + AddLabel(150, 170, LabelHue, Firewall.FirewallSetCount.ToString()); AddLabel(20, 190, LabelHue, "Clients:"); AddLabel(150, 190, LabelHue, NetState.Instances.Count.ToString()); @@ -1165,7 +1165,16 @@ namespace Server.Gumps { AddFirewallHeader(); - m_List ??= Firewall.FirewallSet.ToList(); + if (m_List == null) + { + Firewall.ReadFirewallSet(firewallSet => + { + list = new List(firewallSet.Count); + list.AddRange(firewallSet); + }); + + m_List = list; + } AddLabelCropped(12, 120, 358, 20, LabelHue, "IP Address"); @@ -1178,7 +1187,7 @@ namespace Server.Gumps AddImage(375, 122, 0x25EA); } - if ((listPage + 1) * 12 < m_List.Count) + if ((listPage + 1) * 12 < m_List!.Count) { AddButton(392, 122, 0x15E1, 0x15E5, GetButtonID(1, 1)); } @@ -1313,7 +1322,7 @@ namespace Server.Gumps public void AddPageButton( int x, int y, int buttonID, string text, AdminGumpPage page, - params AdminGumpPage[] subPages + params ReadOnlySpan subPages ) { var isSelection = m_PageType == page; @@ -3470,19 +3479,22 @@ namespace Server.Gumps if (string.IsNullOrEmpty(match)) { - notice = "You must enter a username to search."; + notice = "You must enter an IP to search."; } else { - foreach (var check in Firewall.FirewallSet) + Firewall.ReadFirewallSet(firewallSet => { - var checkStr = check.ToString(); - - if (checkStr.ContainsOrdinal(match)) + foreach (var check in firewallSet) { - results.Add(check); + var checkStr = check.ToString(); + + if (checkStr.ContainsOrdinal(match)) + { + results.Add(check); + } } - } + }); } if (results.Count == 1) diff --git a/Projects/UOContent/Gumps/Base/BaseGump.cs b/Projects/UOContent/Gumps/Base/BaseGump.cs index a3b393b7a..439146a3c 100644 --- a/Projects/UOContent/Gumps/Base/BaseGump.cs +++ b/Projects/UOContent/Gumps/Base/BaseGump.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2024 - ModernUO Development Team * + * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: BaseGump.cs * * * @@ -86,4 +86,37 @@ public abstract class BaseGump return hash == 461 ? hash * primeMulti : hash; } } + + public static Point2D GetItemGraphicOffset(int itemId) + { + var (width, height) = ItemBounds.Sizes[itemId]; + var x = 0; + var y = 0; + + if (width > 44) + { + x -= (width - 44) / 2; + } + else if (width < 44) + { + x += (44 - width) / 2; + } + + if (height > 44) + { + y -= height - 44; + } + else if (height < 44) + { + y += 44 - height; + } + + return new Point2D(x, y); + } + + public static Point2D GetGumpOffsetForItemGraphic(int itemId, int relativeX, int relativeY) + { + var offset = GetItemGraphicOffset(itemId); + return new Point2D(relativeX * 22 - relativeY * 22 + offset.X, relativeX * 22 + relativeY * 22 + offset.Y); + } } diff --git a/Projects/UOContent/Gumps/Base/GumpStringsBuilder.cs b/Projects/UOContent/Gumps/Base/GumpStringsBuilder.cs index 798dc6a51..0e818f161 100644 --- a/Projects/UOContent/Gumps/Base/GumpStringsBuilder.cs +++ b/Projects/UOContent/Gumps/Base/GumpStringsBuilder.cs @@ -64,6 +64,82 @@ public ref struct GumpStringsBuilder } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetHtmlText( + ref RawInterpolatedStringHandler slotKeyHandler, ref RawInterpolatedStringHandler handler, int color, int size = -1, + byte fontStyle = 0 + ) + { + SetHtmlText(ref slotKeyHandler, handler.Text, color, size, fontStyle); + handler.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetHtmlText( + ref RawInterpolatedStringHandler slotKeyHandler, ReadOnlySpan value, int color, int size = -1, byte fontStyle = 0 + ) + { + SetHtmlText(slotKeyHandler.Text, value, color, size, fontStyle); + slotKeyHandler.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetHtmlText( + ReadOnlySpan slotKey, ref RawInterpolatedStringHandler handler, int color, int size = -1, byte fontStyle = 0 + ) + { + SetHtmlText(slotKey, handler.Text, color, size, fontStyle); + handler.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetHtmlText( + ReadOnlySpan slotKey, ReadOnlySpan value, int color, int size = -1, byte fontStyle = 0 + ) + { + var coloredTextHandler = value.Color(color, size, fontStyle); + SetStringSlot(slotKey, ref coloredTextHandler); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetHtmlTextCentered( + ref RawInterpolatedStringHandler slotKeyHandler, ref RawInterpolatedStringHandler handler, int color = -1, + int size = -1, byte fontStyle = 0 + ) + { + SetHtmlTextCentered(ref slotKeyHandler, handler.Text, color, size, fontStyle); + handler.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetHtmlTextCentered( + ref RawInterpolatedStringHandler slotKeyHandler, ReadOnlySpan value, int color = -1, int size = -1, + byte fontStyle = 0 + ) + { + SetHtmlTextCentered(slotKeyHandler.Text, value, color, size, fontStyle); + slotKeyHandler.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetHtmlTextCentered( + ReadOnlySpan slotKey, ref RawInterpolatedStringHandler handler, int color = -1, int size = -1, + byte fontStyle = 0 + ) + { + SetHtmlTextCentered(slotKey, handler.Text, color, size, fontStyle); + handler.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetHtmlTextCentered( + ReadOnlySpan slotKey, ReadOnlySpan value, int color = -1, int size = -1, byte fontStyle = 0 + ) + { + var coloredTextHandler = value.Center(color, size, fontStyle); + SetStringSlot(slotKey, ref coloredTextHandler); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetStringSlot(ReadOnlySpan slotKey, ref RawInterpolatedStringHandler handler) { @@ -71,6 +147,20 @@ public ref struct GumpStringsBuilder handler.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetStringSlot(ref RawInterpolatedStringHandler slotKeyHandler, ref RawInterpolatedStringHandler handler) + { + SetStringSlot(ref slotKeyHandler, handler.Text); + handler.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetStringSlot(ref RawInterpolatedStringHandler slotKeyHandler, ReadOnlySpan text) + { + SetStringSlot(slotKeyHandler.Text, text); + slotKeyHandler.Clear(); + } + public void SetStringSlot(ReadOnlySpan slotKey, ReadOnlySpan text) { var hash = HashUtility.ComputeHash64(slotKey); diff --git a/Projects/UOContent/Gumps/Base/StaticGump.cs b/Projects/UOContent/Gumps/Base/StaticGump.cs index c0f5eb920..8b428ab0b 100644 --- a/Projects/UOContent/Gumps/Base/StaticGump.cs +++ b/Projects/UOContent/Gumps/Base/StaticGump.cs @@ -33,6 +33,9 @@ public abstract class StaticGump : BaseGump where TSelf : StaticGump true; + public override int Switches => _switches; public override int TextEntries => _textEntries; @@ -67,7 +70,7 @@ public abstract class StaticGump : BaseGump where TSelf : StaticGump : BaseGump where TSelf : StaticGump.Shared.Rent(_staticStrings.Length + stringsData.Length); + var stringsLength = _staticStrings.Length + stringsData.Length; + + var buffer = STArrayPool.Shared.Rent(stringsLength); _staticStrings.CopyTo(buffer.AsSpan()); stringsData.CopyTo(buffer.AsSpan(_staticStrings.Length)); - OutgoingGumpPackets.WritePacked(buffer, ref writer); + OutgoingGumpPackets.WritePacked(buffer.AsSpan(0, stringsLength), ref writer); STArrayPool.Shared.Return(buffer); } } diff --git a/Projects/UOContent/Gumps/Base/StaticGumpBuilder.cs b/Projects/UOContent/Gumps/Base/StaticGumpBuilder.cs index c05e03f87..5e7702dab 100644 --- a/Projects/UOContent/Gumps/Base/StaticGumpBuilder.cs +++ b/Projects/UOContent/Gumps/Base/StaticGumpBuilder.cs @@ -312,7 +312,7 @@ public ref struct StaticGumpBuilder [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddLabelPlaceholder(int x, int y, int hue, ref RawInterpolatedStringHandler handler) { - AddHtmlPlaceholder(x, y, 0, 0, handler.Text); + AddLabelPlaceholder(x, y, hue, handler.Text); handler.Clear(); } diff --git a/Projects/UOContent/Gumps/Guilds/New Guild System/BaseGuildGump.cs b/Projects/UOContent/Gumps/Guilds/New Guild System/BaseGuildGump.cs index ee4c36dce..9aa1b5152 100644 --- a/Projects/UOContent/Gumps/Guilds/New Guild System/BaseGuildGump.cs +++ b/Projects/UOContent/Gumps/Guilds/New Guild System/BaseGuildGump.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Server.Gumps; using Server.Misc; using Server.Mobiles; @@ -78,55 +79,20 @@ namespace Server.Guilds !(m.Deleted || g.Disbanded || m is not PlayerMobile || m.AccessLevel < AccessLevel.GameMaster && !g.IsMember(m)); - public static bool CheckProfanity(string s, int maxLength = 50) - { - // return NameVerification.Validate( s, 1, 50, true, true, false, int.MaxValue, ProfanityProtection.Exceptions, ProfanityProtection.Disallowed, ProfanityProtection.StartDisallowed ); //What am I doing wrong, this still allows chars like the <3 symbol... 3 AM. someone change this to use this - - // With testing on OSI, Guild stuff seems to follow a 'simpler' method of profanity protection - if (s.Length < 1 || s.Length > maxLength) - { - return false; - } - - var exceptions = ProfanityProtection.Exceptions; - - s = s.ToLower(); - - for (var i = 0; i < s.Length; ++i) - { - var c = s[i]; - - if (c is < 'a' or > 'z' && c is < '0' or > '9') - { - var except = false; - - for (var j = 0; !except && j < exceptions.Length; j++) - { - if (c == exceptions[j]) - { - except = true; - } - } - - if (!except) - { - return false; - } - } - } - - var disallowed = ProfanityProtection.Disallowed; - - for (var i = 0; i < disallowed.Length; i++) - { - if (s.IndexOfOrdinal(disallowed[i]) != -1) - { - return false; - } - } - - return true; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CheckProfanity(string s, int maxLength = 50) => + NameVerification.Validate( + s, + 1, + maxLength, + true, + true, + false, + 0, + ProfanityProtection.Exceptions, + ProfanityProtection.Disallowed, + ProfanityProtection.DisallowedSearchValues + ); public void AddHtmlText(int x, int y, int width, int height, TextDefinition text, bool back, bool scroll) { diff --git a/Projects/UOContent/Gumps/PetResurrectGump.cs b/Projects/UOContent/Gumps/PetResurrectGump.cs index c2c28ded4..103ed8255 100644 --- a/Projects/UOContent/Gumps/PetResurrectGump.cs +++ b/Projects/UOContent/Gumps/PetResurrectGump.cs @@ -39,7 +39,7 @@ public class PetResurrectGump : StaticGump protected override void BuildStrings(ref GumpStringsBuilder builder) { - builder.SetStringSlot("petName", $"
{_pet.Name}
"); + builder.SetHtmlTextCentered("petName", _pet.Name); } public override void OnResponse(NetState state, in RelayInfo info) diff --git a/Projects/UOContent/Gumps/PolymorphGump.cs b/Projects/UOContent/Gumps/PolymorphGump.cs index 0bd001ffe..4a148785a 100644 --- a/Projects/UOContent/Gumps/PolymorphGump.cs +++ b/Projects/UOContent/Gumps/PolymorphGump.cs @@ -137,7 +137,7 @@ namespace Server.Gumps var entries = Categories[cat]; - if (ent < 0 || ent >= entries.Entries.Length) + if (ent >= 0 && ent < entries.Entries.Length) { Spell spell = new PolymorphSpell(state.Mobile, _scroll, entries.Entries[ent].BodyID); spell.Cast(); diff --git a/Projects/UOContent/Gumps/Props/PropsGump.cs b/Projects/UOContent/Gumps/Props/PropsGump.cs index 381f28176..8f7d31119 100644 --- a/Projects/UOContent/Gumps/Props/PropsGump.cs +++ b/Projects/UOContent/Gumps/Props/PropsGump.cs @@ -299,7 +299,8 @@ namespace Server.Gumps return; } - var type = prop.PropertyType; + var obj = prop.GetValue(m_Object, null); + var type = obj?.GetType() ?? prop.PropertyType; if (IsType(type, OfEntity)) { @@ -392,8 +393,6 @@ namespace Server.Gumps } else if (HasAttribute(type, OfPropertyObject, true)) { - var obj = prop.GetValue(m_Object, null); - from.SendGump( obj != null ? new PropertiesGump(from, obj, m_Stack, new StackEntry(m_Object, prop)) diff --git a/Projects/UOContent/Gumps/RewardGump.cs b/Projects/UOContent/Gumps/RewardGump.cs index 9d4709fba..53a3f8038 100644 --- a/Projects/UOContent/Gumps/RewardGump.cs +++ b/Projects/UOContent/Gumps/RewardGump.cs @@ -62,7 +62,7 @@ namespace Server.Gumps { var entry = Rewards[i]; - var bounds = ItemBounds.Table[entry.ItemID]; + var bounds = ItemBounds.Bounds[entry.ItemID]; var height = Math.Max(36, bounds.Height); if (offset + height > 320) diff --git a/Projects/UOContent/Gumps/SelectAddonDirectionGump.cs b/Projects/UOContent/Gumps/SelectAddonDirectionGump.cs new file mode 100644 index 000000000..3ef6164f2 --- /dev/null +++ b/Projects/UOContent/Gumps/SelectAddonDirectionGump.cs @@ -0,0 +1,49 @@ +using Server.Items; +using Server.Network; + +namespace Server.Gumps; + +public abstract class SelectAddonDirectionGump : StaticGump where T : SelectAddonDirectionGump +{ + private readonly IDirectionAddonDeed _deed; + + public SelectAddonDirectionGump(IDirectionAddonDeed deed) : base(60, 63) => _deed = deed; + + public override bool Singleton => false; + + public abstract int SelectionNumber { get; } + + protected override void BuildLayout(ref StaticGumpBuilder builder) + { + builder.AddPage(); + + builder.AddBackground(0, 0, 273, 324, 0x13BE); + builder.AddImageTiled(10, 10, 253, 20, 0xA40); + builder.AddImageTiled(10, 40, 253, 244, 0xA40); + builder.AddImageTiled(10, 294, 253, 20, 0xA40); + builder.AddAlphaRegion(10, 10, 253, 304); + + builder.AddButton(10, 294, 0xFB1, 0xFB2, 0); + + builder.AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL + builder.AddHtmlLocalized(14, 12, 273, 20, SelectionNumber, 0x7FFF); + + builder.AddPage(1); + + builder.AddButton(19, 49, 0x845, 0x846, 1); + builder.AddHtmlLocalized(44, 47, 213, 20, 1075386, 0x7FFF); // South + builder.AddButton(19, 73, 0x845, 0x846, 2); + builder.AddHtmlLocalized(44, 71, 213, 20, 1075387, 0x7FFF); // East + } + + public override void OnResponse(NetState sender, in RelayInfo info) + { + if (_deed?.Deleted != false || info.ButtonID == 0) + { + return; + } + + _deed.East = info.ButtonID != 1; + _deed.SendTarget(sender.Mobile); + } +} diff --git a/Projects/UOContent/Gumps/StaticWarningGump.cs b/Projects/UOContent/Gumps/StaticWarningGump.cs index ef98d4adc..1129fb36b 100644 --- a/Projects/UOContent/Gumps/StaticWarningGump.cs +++ b/Projects/UOContent/Gumps/StaticWarningGump.cs @@ -88,7 +88,7 @@ public abstract class StaticWarningGump : StaticGump where T : StaticWarni protected sealed override void BuildStrings(ref GumpStringsBuilder builder) { - builder.SetStringSlot("content", Content.Color(ContentColor)); + builder.SetHtmlText("content", Content, ContentColor); } public override void OnResponse(NetState sender, in RelayInfo info) => _callback?.Invoke(info.ButtonID == 1); diff --git a/Projects/UOContent/Gumps/TithingGump.cs b/Projects/UOContent/Gumps/TithingGump.cs index c917ccff2..b68a4a150 100644 --- a/Projects/UOContent/Gumps/TithingGump.cs +++ b/Projects/UOContent/Gumps/TithingGump.cs @@ -56,8 +56,8 @@ public class TithingGump : StaticGump // Just in case _offer = Math.Clamp(_offer, 0, totalGold); - builder.SetStringSlot("goldOffer", (totalGold - _offer).ToString("N0")); - builder.SetStringSlot("titheOffer", _offer.ToString("N0")); + builder.SetStringSlot("goldOffer", $"{totalGold - _offer:N0}"); + builder.SetStringSlot("titheOffer", $"{_offer:N0}"); } public override void OnResponse(NetState sender, in RelayInfo info) diff --git a/Projects/UOContent/Gumps/VendorRentalGumps.cs b/Projects/UOContent/Gumps/VendorRentalGumps.cs index 589cdb60c..8b519b5f7 100644 --- a/Projects/UOContent/Gumps/VendorRentalGumps.cs +++ b/Projects/UOContent/Gumps/VendorRentalGumps.cs @@ -460,7 +460,7 @@ namespace Server.Gumps goldToGive = 0; } - PlayerVendor vendor = new RentedVendor( + var vendor = new RentedVendor( from, house, m_Contract.Duration, @@ -472,13 +472,11 @@ namespace Server.Gumps m_Contract.Delete(); - from.SendLocalizedMessage( - 1062377 - ); // You have accepted the offer and now own a vendor in this house. Rental contract options and details may be viewed on this vendor via the 'Contract Options' context menu. - m_Landlord.SendLocalizedMessage( - 1062376, - from.Name - ); // ~1_NAME~ has accepted your vendor rental offer. Rental contract details and options may be viewed on this vendor via the 'Contract Options' context menu. + // You have accepted the offer and now own a vendor in this house. Rental contract options and details may be viewed on this vendor via the 'Contract Options' context menu. + from.SendLocalizedMessage(1062377); + + // ~1_NAME~ has accepted your vendor rental offer. Rental contract details and options may be viewed on this vendor via the 'Contract Options' context menu. + m_Landlord.SendLocalizedMessage(1062376, from.Name); } protected override void Cancel(Mobile from) diff --git a/Projects/UOContent/Holiday Stuff/Christmas/2004/Mistletoe.cs b/Projects/UOContent/Holiday Stuff/Christmas/2004/Mistletoe.cs index f52e49dd7..c5927a129 100644 --- a/Projects/UOContent/Holiday Stuff/Christmas/2004/Mistletoe.cs +++ b/Projects/UOContent/Holiday Stuff/Christmas/2004/Mistletoe.cs @@ -226,7 +226,7 @@ public partial class MistletoeDeed : Item if (northWall && westWall) { - from.SendGump(new MistletoeDeedGump(from, loc, this)); + from.SendGump(new MistletoeDeedGump(loc, this)); } else { @@ -266,9 +266,11 @@ public partial class MistletoeDeed : Item if (itemID > 0) { - Item addon = new MistletoeAddon(Hue); + var addon = new MistletoeAddon(Hue) + { + ItemID = itemID + }; - addon.ItemID = itemID; addon.MoveToWorld(loc, from.Map); house.Addons.Add(addon); @@ -276,47 +278,47 @@ public partial class MistletoeDeed : Item } } - private class MistletoeDeedGump : Gump + private class MistletoeDeedGump : StaticGump { private readonly MistletoeDeed _deed; - private readonly Mobile _from; private readonly Point3D _loc; - public MistletoeDeedGump(Mobile from, Point3D loc, MistletoeDeed deed) : base(150, 50) + public override bool Singleton => false; + + public MistletoeDeedGump(Point3D loc, MistletoeDeed deed) : base(150, 50) { - _from = from; _loc = loc; _deed = deed; + } - AddBackground(0, 0, 300, 150, 0xA28); + protected override void BuildLayout(ref StaticGumpBuilder builder) + { + builder.AddBackground(0, 0, 300, 150, 0xA28); - AddPage(0); + builder.AddPage(); - AddItem(90, 30, 0x2375); - AddItem(180, 30, 0x2374); - AddButton(50, 35, 0x868, 0x869, 1); - AddButton(145, 35, 0x868, 0x869, 2); + builder.AddItem(90, 30, 0x2375); + builder.AddItem(180, 30, 0x2374); + builder.AddButton(50, 35, 0x868, 0x869, 1); + builder.AddButton(145, 35, 0x868, 0x869, 2); } public override void OnResponse(NetState sender, in RelayInfo info) { - if (_deed.Deleted) + if (_deed.Deleted || info.ButtonID == 0) { return; } - switch (info.ButtonID) + var from = sender.Mobile; + + if (info.ButtonID == 1) { - case 1: - { - _deed.PlaceAddon(_from, _loc, false, true); - break; - } - case 2: - { - _deed.PlaceAddon(_from, _loc, true, false); - break; - } + _deed.PlaceAddon(from, _loc, false, true); + } + else + { + _deed.PlaceAddon(from, _loc, true, false); } } } diff --git a/Projects/UOContent/Holiday Stuff/Christmas/2010/Addons/FireFliesDeed.cs b/Projects/UOContent/Holiday Stuff/Christmas/2010/Addons/FireFliesDeed.cs index 948e15196..c930a18cb 100644 --- a/Projects/UOContent/Holiday Stuff/Christmas/2010/Addons/FireFliesDeed.cs +++ b/Projects/UOContent/Holiday Stuff/Christmas/2010/Addons/FireFliesDeed.cs @@ -85,35 +85,27 @@ public partial class FirefliesDeed : Item return; } - from.SendGump(new FacingGump(this, from)); + from.SendGump(new FacingGump(this)); } - private class FacingGump : Gump + private class FacingGump : StaticGump { private readonly FirefliesDeed _deed; - private readonly Mobile _placer; public override bool Singleton => true; - public FacingGump(FirefliesDeed deed, Mobile player) : base(150, 50) + public FacingGump(FirefliesDeed deed) : base(150, 50) => _deed = deed; + + protected override void BuildLayout(ref StaticGumpBuilder builder) { - _deed = deed; - _placer = player; + builder.AddBackground(0, 0, 300, 150, 0xA28); + builder.AddPage(); - Closable = true; - Disposable = true; - Draggable = true; - Resizable = false; + builder.AddItem(90, 30, 0x2332); + builder.AddItem(180, 30, 0x2336); - AddPage(0); - - AddBackground(0, 0, 300, 150, 0xA28); - - AddItem(90, 30, 0x2332); - AddItem(180, 30, 0x2336); - - AddButton(50, 35, 0x868, 0x869, (int)Buttons.East); - AddButton(145, 35, 0x868, 0x869, (int)Buttons.South); + builder.AddButton(50, 35, 0x868, 0x869, (int)Buttons.East); + builder.AddButton(145, 35, 0x868, 0x869, (int)Buttons.South); } public override void OnResponse(NetState sender, in RelayInfo info) @@ -130,7 +122,12 @@ public partial class FirefliesDeed : Item _ => 0 }; - _placer.Target = new InternalTarget(_deed, itemId); + if (itemId == 0) + { + return; + } + + sender.Mobile.Target = new InternalTarget(_deed, itemId); } private enum Buttons diff --git a/Projects/UOContent/Holiday Stuff/Halloween/2006/Engines/TrickOrTreat.cs b/Projects/UOContent/Holiday Stuff/Halloween/2006/Engines/TrickOrTreat.cs index 7976ad583..af6ffc439 100644 --- a/Projects/UOContent/Holiday Stuff/Halloween/2006/Engines/TrickOrTreat.cs +++ b/Projects/UOContent/Holiday Stuff/Halloween/2006/Engines/TrickOrTreat.cs @@ -76,7 +76,7 @@ namespace Server.Engines.Events if (CheckMobile(m_From)) { - Mobile twin = new NaughtyTwin(m_From); + var twin = new NaughtyTwin(m_From); if (twin.Deleted) { diff --git a/Projects/UOContent/Items/Armor/BaseArmor.cs b/Projects/UOContent/Items/Armor/BaseArmor.cs index a6955bf0f..b546a3774 100644 --- a/Projects/UOContent/Items/Armor/BaseArmor.cs +++ b/Projects/UOContent/Items/Armor/BaseArmor.cs @@ -1082,67 +1082,69 @@ namespace Server.Items public override bool CanEquip(Mobile from) { + if (!from.Player || from.AccessLevel >= AccessLevel.GameMaster) + { + return base.CanEquip(from); + } + if (!Ethic.CheckEquip(from, this)) { return false; } - if (from.AccessLevel < AccessLevel.GameMaster) + if (!CheckRace(from)) { - if (!CheckRace(from)) + return false; + } + + if (!AllowMaleWearer && !from.Female) + { + if (AllowFemaleWearer) { - return false; + from.SendLocalizedMessage(1010388); // Only females can wear this. + } + else + { + from.SendMessage("You may not wear this."); } - if (!AllowMaleWearer && !from.Female) - { - if (AllowFemaleWearer) - { - from.SendLocalizedMessage(1010388); // Only females can wear this. - } - else - { - from.SendMessage("You may not wear this."); - } + return false; + } - return false; + if (!AllowFemaleWearer && from.Female) + { + if (AllowMaleWearer) + { + from.SendLocalizedMessage(1063343); // Only males can wear this. + } + else + { + from.SendMessage("You may not wear this."); } - if (!AllowFemaleWearer && from.Female) - { - if (AllowMaleWearer) - { - from.SendLocalizedMessage(1063343); // Only males can wear this. - } - else - { - from.SendMessage("You may not wear this."); - } + return false; + } - return false; - } + int strBonus = ComputeStatBonus(StatType.Str), strReq = ComputeStatReq(StatType.Str); + int dexBonus = ComputeStatBonus(StatType.Dex), dexReq = ComputeStatReq(StatType.Dex); + int intBonus = ComputeStatBonus(StatType.Int), intReq = ComputeStatReq(StatType.Int); - int strBonus = ComputeStatBonus(StatType.Str), strReq = ComputeStatReq(StatType.Str); - int dexBonus = ComputeStatBonus(StatType.Dex), dexReq = ComputeStatReq(StatType.Dex); - int intBonus = ComputeStatBonus(StatType.Int), intReq = ComputeStatReq(StatType.Int); + if (from.Dex < dexReq || from.Dex + dexBonus < 1) + { + from.SendLocalizedMessage(502077); // You do not have enough dexterity to equip this item. + return false; + } - if (from.Dex < dexReq || from.Dex + dexBonus < 1) - { - from.SendLocalizedMessage(502077); // You do not have enough dexterity to equip this item. - return false; - } + if (from.Str < strReq || from.Str + strBonus < 1) + { + from.SendLocalizedMessage(500213); // You are not strong enough to equip that. + return false; + } - if (from.Str < strReq || from.Str + strBonus < 1) - { - from.SendLocalizedMessage(500213); // You are not strong enough to equip that. - return false; - } - - if (from.Int < intReq || from.Int + intBonus < 1) - { - from.SendMessage("You are not smart enough to equip that."); - return false; - } + if (from.Int < intReq || from.Int + intBonus < 1) + { + from.SendMessage("You are not smart enough to equip that."); + return false; } return base.CanEquip(from); diff --git a/Projects/UOContent/Items/Clothing/BaseClothing.cs b/Projects/UOContent/Items/Clothing/BaseClothing.cs index 5b249e490..ce77d2ec6 100644 --- a/Projects/UOContent/Items/Clothing/BaseClothing.cs +++ b/Projects/UOContent/Items/Clothing/BaseClothing.cs @@ -429,63 +429,65 @@ namespace Server.Items public override bool CanEquip(Mobile from) { + if (!from.Player || from.AccessLevel >= AccessLevel.GameMaster) + { + return base.CanEquip(from); + } + if (!Ethic.CheckEquip(from, this)) { return false; } - if (from.AccessLevel < AccessLevel.GameMaster) + if (RequiredRace != null && from.Race != RequiredRace) { - if (RequiredRace != null && from.Race != RequiredRace) + if (RequiredRace == Race.Elf) { - if (RequiredRace == Race.Elf) - { - from.SendLocalizedMessage(1072203); // Only Elves may use this. - } - else - { - from.SendMessage($"Only {RequiredRace.PluralName} may use this."); - } - - return false; + from.SendLocalizedMessage(1072203); // Only Elves may use this. + } + else + { + from.SendMessage($"Only {RequiredRace.PluralName} may use this."); } - if (!AllowMaleWearer && !from.Female) - { - if (AllowFemaleWearer) - { - from.SendLocalizedMessage(1010388); // Only females can wear this. - } - else - { - from.SendMessage("You may not wear this."); - } + return false; + } - return false; + if (!AllowMaleWearer && !from.Female) + { + if (AllowFemaleWearer) + { + from.SendLocalizedMessage(1010388); // Only females can wear this. + } + else + { + from.SendMessage("You may not wear this."); } - if (!AllowFemaleWearer && from.Female) - { - if (AllowMaleWearer) - { - from.SendLocalizedMessage(1063343); // Only males can wear this. - } - else - { - from.SendMessage("You may not wear this."); - } + return false; + } - return false; + if (!AllowFemaleWearer && from.Female) + { + if (AllowMaleWearer) + { + from.SendLocalizedMessage(1063343); // Only males can wear this. + } + else + { + from.SendMessage("You may not wear this."); } - var strBonus = ComputeStatBonus(StatType.Str); - var strReq = ComputeStatReq(StatType.Str); + return false; + } - if (from.Str < strReq || from.Str + strBonus < 1) - { - from.SendLocalizedMessage(500213); // You are not strong enough to equip that. - return false; - } + var strBonus = ComputeStatBonus(StatType.Str); + var strReq = ComputeStatReq(StatType.Str); + + if (from.Str < strReq || from.Str + strBonus < 1) + { + from.SendLocalizedMessage(500213); // You are not strong enough to equip that. + return false; } return base.CanEquip(from); diff --git a/Projects/UOContent/Items/Construction/Doors/BaseDoor.cs b/Projects/UOContent/Items/Construction/Doors/BaseDoor.cs index 5997ab735..07796c900 100644 --- a/Projects/UOContent/Items/Construction/Doors/BaseDoor.cs +++ b/Projects/UOContent/Items/Construction/Doors/BaseDoor.cs @@ -423,6 +423,13 @@ public abstract partial class BaseDoor : Item, ILockable, ITelekinesisable } } + public override void OnDelete() + { + _timer?.Stop(); + _timer = null; + Link = null; + } + [AfterDeserialization] private void AfterDeserialization() { @@ -442,6 +449,12 @@ public abstract partial class BaseDoor : Item, ILockable, ITelekinesisable protected override void OnTick() { + if (_door.Deleted) + { + Stop(); + return; + } + if (_door.Open && _door.IsFreeToClose()) { _door.Open = false; diff --git a/Projects/UOContent/Items/Containers/Strongbox.cs b/Projects/UOContent/Items/Containers/Strongbox.cs index 08d6778a0..6e937ff9c 100644 --- a/Projects/UOContent/Items/Containers/Strongbox.cs +++ b/Projects/UOContent/Items/Containers/Strongbox.cs @@ -101,7 +101,7 @@ public partial class StrongBox : BaseContainer, IChoppable public Container ConvertToStandardContainer() { - Container metalBox = new MetalBox(); + var metalBox = new MetalBox(); var subItems = new List(Items); foreach (var subItem in subItems) diff --git a/Projects/UOContent/Items/Deeds/BarkeepContract.cs b/Projects/UOContent/Items/Deeds/BarkeepContract.cs index 2ac3b0499..3db975f11 100644 --- a/Projects/UOContent/Items/Deeds/BarkeepContract.cs +++ b/Projects/UOContent/Items/Deeds/BarkeepContract.cs @@ -26,9 +26,11 @@ public partial class BarkeepContract : Item { from.SendLocalizedMessage(503248); // Your godly powers allow you to place this vendor whereever you wish. - Mobile v = new PlayerBarkeeper(from, BaseHouse.FindHouseAt(from)); + var v = new PlayerBarkeeper(from, BaseHouse.FindHouseAt(from)) + { + Direction = from.Direction & Direction.Mask + }; - v.Direction = from.Direction & Direction.Mask; v.MoveToWorld(from.Location, from.Map); Delete(); @@ -68,9 +70,11 @@ public partial class BarkeepContract : Item } else { - Mobile v = new PlayerBarkeeper(from, house); + var v = new PlayerBarkeeper(from, house) + { + Direction = from.Direction & Direction.Mask + }; - v.Direction = from.Direction & Direction.Mask; v.MoveToWorld(from.Location, from.Map); Delete(); diff --git a/Projects/UOContent/Items/Deeds/IDirectionAddonDeed.cs b/Projects/UOContent/Items/Deeds/IDirectionAddonDeed.cs new file mode 100644 index 000000000..bdb438771 --- /dev/null +++ b/Projects/UOContent/Items/Deeds/IDirectionAddonDeed.cs @@ -0,0 +1,8 @@ +namespace Server.Items; + +public interface IDirectionAddonDeed : IEntity +{ + public bool East { get; set; } + + public void SendTarget(Mobile m); +} diff --git a/Projects/UOContent/Items/Deeds/NameChangeDeed.cs b/Projects/UOContent/Items/Deeds/NameChangeDeed.cs index b21320604..0b2d2aa31 100644 --- a/Projects/UOContent/Items/Deeds/NameChangeDeed.cs +++ b/Projects/UOContent/Items/Deeds/NameChangeDeed.cs @@ -1,3 +1,4 @@ +using System; using ModernUO.Serialization; using Server.Gumps; using Server.Misc; @@ -79,15 +80,15 @@ public class NameChangeDeedGump : Gump var m = sender.Mobile; - var newName = info.GetTextEntry(0)?.Trim(); + var newName = info.GetTextEntry(0).AsSpan().Trim(); - if (!NameVerification.Validate(newName, 2, 16, true, false, true, 1, NameVerification.SpaceDashPeriodQuote)) + if (!NameVerification.ValidatePlayerName(newName)) { m.SendMessage("That name is unacceptable."); return; } - m.RawName = newName; + m.RawName = newName.ToString(); m.SendMessage("Your name has been changed!"); m.SendMessage($"You are now known as {newName}"); m_Sender.Delete(); diff --git a/Projects/UOContent/Items/Misc/Corpses/Corpse.cs b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs index 8ea7d1319..4c2f1ff26 100644 --- a/Projects/UOContent/Items/Misc/Corpses/Corpse.cs +++ b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs @@ -109,7 +109,7 @@ public partial class Corpse : Container, ICarvable [SerializableField(7, setter: "private")] private List _aggressors; - [SerializableField(8, setter: "private")] + [SerializableField(8, setter: "protected")] [SerializedCommandProperty(AccessLevel.GameMaster)] private Mobile _owner; @@ -912,7 +912,7 @@ public partial class Corpse : Container, ICarvable var obj = qs.FindObjective(); if (obj?.CorpseWithBone == this && (!obj.Completed || UzeraanTurmoilQuest.HasLostDaemonBone(player))) { - Item bone = new QuestDaemonBone(); + var bone = new QuestDaemonBone(); if (player.PlaceInBackpack(bone)) { diff --git a/Projects/UOContent/Items/Misc/FlippableAddonAttribute.cs b/Projects/UOContent/Items/Misc/FlippableAddonAttribute.cs index 2a76571ea..1d929904b 100644 --- a/Projects/UOContent/Items/Misc/FlippableAddonAttribute.cs +++ b/Projects/UOContent/Items/Misc/FlippableAddonAttribute.cs @@ -6,7 +6,7 @@ namespace Server.Items; [AttributeUsage(AttributeTargets.Class)] public class FlippableAddonAttribute : Attribute { - private static readonly string m_MethodName = "Flip"; + private const string MethodName = "Flip"; private static readonly Type[] m_Params = { @@ -26,7 +26,7 @@ public class FlippableAddonAttribute : Attribute try { - var flipMethod = addon.GetType().GetMethod(m_MethodName, m_Params); + var flipMethod = addon.GetType().GetMethod(MethodName, m_Params); if (flipMethod == null) { diff --git a/Projects/UOContent/Items/Misc/HairDye.cs b/Projects/UOContent/Items/Misc/HairDye.cs index a66bcde2e..2693250a0 100644 --- a/Projects/UOContent/Items/Misc/HairDye.cs +++ b/Projects/UOContent/Items/Misc/HairDye.cs @@ -26,58 +26,65 @@ public partial class HairDye : Item } } -public class HairDyeGump : Gump +public class HairDyeGump : StaticGump { - private static HairDyeEntry[] _entries = - { - new("*****", 1602, 26), - new("*****", 1628, 27), - new("*****", 1502, 32), - new("*****", 1302, 32), - new("*****", 1402, 32), - new("*****", 1202, 24), - new("*****", 2402, 29), - new("*****", 2213, 6), - new("*****", 1102, 8), - new("*****", 1110, 8), - new("*****", 1118, 16), - new("*****", 1134, 16) - }; + private static readonly (int HueStart, int HueCount)[] _entries = + [ + (1602, 26), + (1628, 27), + (1502, 32), + (1302, 32), + (1402, 32), + (1202, 24), + (2402, 29), + (2213, 6), + (1102, 8), + (1110, 8), + (1118, 16), + (1134, 16) + ]; - private HairDye _hairDye; + private readonly HairDye _hairDye; public override bool Singleton => true; - public HairDyeGump(HairDye dye) : base(50, 50) + public HairDyeGump(HairDye dye) : base(50, 50) => _hairDye = dye; + + protected override void BuildLayout(ref StaticGumpBuilder builder) { - _hairDye = dye; + builder.AddPage(); - AddPage(0); + builder.AddBackground(100, 10, 350, 355, 2600); + builder.AddBackground(120, 54, 110, 270, 5100); - AddBackground(100, 10, 350, 355, 2600); - AddBackground(120, 54, 110, 270, 5100); + builder.AddHtmlLocalized(70, 25, 400, 35, 1011013); //
Hair Color Selection Menu
- AddHtmlLocalized(70, 25, 400, 35, 1011013); //
Hair Color Selection Menu
+ builder.AddButton(149, 328, 4005, 4007, 1); + builder.AddHtmlLocalized(185, 329, 250, 35, 1011014); // Dye my hair this color! - AddButton(149, 328, 4005, 4007, 1); - AddHtmlLocalized(185, 329, 250, 35, 1011014); // Dye my hair this color! - - for (var i = 0; i < _entries.Length; ++i) + ReadOnlySpan<(int HueStart, int HueCount)> entries = _entries; + for (var i = 0; i < entries.Length; ++i) { - AddLabel(130, 59 + i * 22, _entries[i].HueStart - 1, _entries[i].Name); - AddButton(207, 60 + i * 22, 5224, 5224, 0, GumpButtonType.Page, i + 1); + var y = 59 + i * 22; + builder.AddLabel(130, y, entries[i].HueStart - 1, "*****"); + builder.AddButton(207, y + 1, 5224, 5224, 0, GumpButtonType.Page, i + 1); } - for (var i = 0; i < _entries.Length; ++i) + for (var i = 0; i < entries.Length; ++i) { - var e = _entries[i]; + var (hueStart, hueCount) = entries[i]; - AddPage(i + 1); + builder.AddPage(i + 1); + var switchId = i * 100; - for (var j = 0; j < e.HueCount; ++j) + for (var j = 0; j < hueCount; ++j) { - AddLabel(278 + j / 16 * 80, 52 + j % 16 * 17, e.HueStart + j - 1, "*****"); - AddRadio(260 + j / 16 * 80, 52 + j % 16 * 17, 210, 211, false, i * 100 + j); + var page = Math.DivRem(j, 16, out var row); + var x = 260 + page * 80; + var y = 52 + row * 17; + + builder.AddRadio(x, y, 210, 211, false, switchId + j); + builder.AddLabel(x + 18, y, hueStart + j - 1, "*****"); } } } @@ -98,54 +105,38 @@ public class HairDyeGump : Gump return; } - if (info.ButtonID != 0 && switches.Length > 0) - { - if (m.HairItemID == 0 && m.FacialHairItemID == 0) - { - m.SendLocalizedMessage(502623); // You have no hair to dye and cannot use this - } - else - { - // To prevent this from being exploited, the hue is abstracted into an internal list - var entryIndex = Math.DivRem(switches[0], 100, out var hueOffset); - - if (entryIndex >= 0 && entryIndex < _entries.Length) - { - var e = _entries[entryIndex]; - - if (hueOffset >= 0 && hueOffset < e.HueCount) - { - var hue = e.HueStart + hueOffset; - - m.HairHue = hue; - m.FacialHairHue = hue; - - m.SendLocalizedMessage(501199); // You dye your hair - _hairDye.Delete(); - m.PlaySound(0x4E); - } - } - } - } - else + if (info.ButtonID == 0 || switches.Length <= 0) { m.SendLocalizedMessage(501200); // You decide not to dye your hair + return; } - } - private class HairDyeEntry - { - public HairDyeEntry(string name, int hueStart, int hueCount) + if (m.HairItemID == 0 && m.FacialHairItemID == 0) { - Name = name; - HueStart = hueStart; - HueCount = hueCount; + m.SendLocalizedMessage(502623); // You have no hair to dye and cannot use this + return; } - public string Name { get; } + // To prevent this from being exploited, the hue is abstracted into an internal list + var entryIndex = Math.DivRem(switches[0], 100, out var hueOffset); - public int HueStart { get; } + if (entryIndex < 0 || entryIndex >= _entries.Length) + { + return; + } - public int HueCount { get; } + var e = _entries[entryIndex]; + + if (hueOffset >= 0 && hueOffset < e.HueCount) + { + var hue = e.HueStart + hueOffset; + + m.HairHue = hue; + m.FacialHairHue = hue; + + m.SendLocalizedMessage(501199); // You dye your hair + _hairDye.Delete(); + m.PlaySound(0x4E); + } } } diff --git a/Projects/UOContent/Items/Misc/PlayerVendorDeed.cs b/Projects/UOContent/Items/Misc/PlayerVendorDeed.cs index ff115b1b8..cf864bce6 100644 --- a/Projects/UOContent/Items/Misc/PlayerVendorDeed.cs +++ b/Projects/UOContent/Items/Misc/PlayerVendorDeed.cs @@ -22,9 +22,11 @@ public partial class ContractOfEmployment : Item { from.SendLocalizedMessage(503248); // Your godly powers allow you to place this vendor whereever you wish. - Mobile v = new PlayerVendor(from, BaseHouse.FindHouseAt(from)); + var v = new PlayerVendor(from, BaseHouse.FindHouseAt(from)) + { + Direction = from.Direction & Direction.Mask + }; - v.Direction = from.Direction & Direction.Mask; v.MoveToWorld(from.Location, from.Map); v.SayTo(from, 503246); // Ah! it feels good to be working again. @@ -69,9 +71,11 @@ public partial class ContractOfEmployment : Item } else { - Mobile v = new PlayerVendor(from, house); + var v = new PlayerVendor(from, house) + { + Direction = from.Direction & Direction.Mask + }; - v.Direction = from.Direction & Direction.Mask; v.MoveToWorld(from.Location, from.Map); v.SayTo(from, 503246); // Ah! it feels good to be working again. diff --git a/Projects/UOContent/Items/Misc/PublicMoongate.cs b/Projects/UOContent/Items/Misc/PublicMoongate.cs index 8355d71e8..8d1f193e2 100644 --- a/Projects/UOContent/Items/Misc/PublicMoongate.cs +++ b/Projects/UOContent/Items/Misc/PublicMoongate.cs @@ -147,7 +147,7 @@ public partial class PublicMoongate : Item foreach (var entry in list.Entries) { - Item item = new PublicMoongate(); + var item = new PublicMoongate(); item.MoveToWorld(entry.Location, list.Map); diff --git a/Projects/UOContent/Items/Misc/SpecialBeardDye.cs b/Projects/UOContent/Items/Misc/SpecialBeardDye.cs index 6fba86026..ee402923f 100644 --- a/Projects/UOContent/Items/Misc/SpecialBeardDye.cs +++ b/Projects/UOContent/Items/Misc/SpecialBeardDye.cs @@ -30,51 +30,59 @@ public partial class SpecialBeardDye : Item } } -public class SpecialBeardDyeGump : Gump +public class SpecialBeardDyeGump : StaticGump { - private static SpecialBeardDyeEntry[] _entries = - { - new("*****", 12, 10), - new("*****", 32, 5), - new("*****", 38, 8), - new("*****", 54, 3), - new("*****", 62, 10), - new("*****", 81, 2), - new("*****", 89, 2), - new("*****", 1153, 2) - }; + private static readonly (int HueStart, int HueCount)[] _entries = + [ + (12, 10), + (32, 5), + (38, 8), + (54, 3), + (62, 10), + (81, 2), + (89, 2), + (1153, 2) + ]; - private SpecialBeardDye _specialBeardDye; + private readonly SpecialBeardDye _specialBeardDye; public override bool Singleton => true; - public SpecialBeardDyeGump(SpecialBeardDye dye) : base(0, 0) + public SpecialBeardDyeGump(SpecialBeardDye dye) : base(0, 0) => _specialBeardDye = dye; + + protected override void BuildLayout(ref StaticGumpBuilder builder) { - _specialBeardDye = dye; + builder.AddPage(); - AddPage(0); - AddBackground(150, 60, 350, 358, 2600); - AddBackground(170, 104, 110, 270, 5100); - AddHtmlLocalized(230, 75, 200, 20, 1011013); // Hair Color Selection Menu - AddHtmlLocalized(235, 380, 300, 20, 1013007); // Dye my beard this color! - AddButton(200, 380, 0xFA5, 0xFA7, 1); // DYE HAIR + builder.AddBackground(150, 60, 350, 358, 2600); + builder.AddBackground(170, 104, 110, 270, 5100); - for (var i = 0; i < _entries.Length; ++i) + builder.AddHtmlLocalized(230, 75, 200, 20, 1011013); //
Hair Color Selection Menu
+ builder.AddHtmlLocalized(235, 380, 300, 20, 1013007); // Dye my beard this color! + builder.AddButton(200, 380, 0xFA5, 0xFA7, 1); // DYE HAIR + + ReadOnlySpan<(int HueStart, int HueCount)> entries = _entries; + for (var i = 0; i < entries.Length; ++i) { - AddLabel(180, 109 + i * 22, _entries[i].HueStart - 1, _entries[i].Name); - AddButton(257, 110 + i * 22, 5224, 5224, 0, GumpButtonType.Page, i + 1); + builder.AddLabel(180, 109 + i * 22, entries[i].HueStart - 1, "*****"); + builder.AddButton(257, 110 + i * 22, 5224, 5224, 0, GumpButtonType.Page, i + 1); } - for (var i = 0; i < _entries.Length; ++i) + for (var i = 0; i < entries.Length; ++i) { - var e = _entries[i]; + var (hueStart, hueCount) = entries[i]; - AddPage(i + 1); + builder.AddPage(i + 1); + var switchId = i * 100; - for (var j = 0; j < e.HueCount; ++j) + for (var j = 0; j < hueCount; ++j) { - AddLabel(328 + j / 16 * 80, 102 + j % 16 * 17, e.HueStart + j - 1, "*****"); - AddRadio(310 + j / 16 * 80, 102 + j % 16 * 17, 210, 211, false, i * 100 + j); + var page = Math.DivRem(j, 16, out var row); + var x = 310 + page * 80; + var y = 102 + row * 17; + + builder.AddRadio(x, y, 210, 211, false, switchId + j); + builder.AddLabel(x + 18, y, hueStart + j - 1, "*****"); } } } @@ -95,53 +103,37 @@ public class SpecialBeardDyeGump : Gump return; } - if (info.ButtonID != 0 && switches.Length > 0) - { - if (m.FacialHairItemID == 0) - { - m.SendLocalizedMessage(502623); // You have no hair to dye and cannot use this - } - else - { - // To prevent this from being exploited, the hue is abstracted into an internal list - var entryIndex = Math.DivRem(switches[0], 100, out var hueOffset); - - if (entryIndex >= 0 && entryIndex < _entries.Length) - { - var e = _entries[entryIndex]; - - if (hueOffset >= 0 && hueOffset < e.HueCount) - { - var hue = e.HueStart + hueOffset; - - m.FacialHairHue = hue; - - m.SendLocalizedMessage(501199); // You dye your hair - _specialBeardDye.Delete(); - m.PlaySound(0x4E); - } - } - } - } - else + if (info.ButtonID == 0 || switches.Length <= 0) { m.SendLocalizedMessage(501200); // You decide not to dye your hair + return; } - } - private class SpecialBeardDyeEntry - { - public SpecialBeardDyeEntry(string name, int hueStart, int hueCount) + if (m.FacialHairItemID == 0) { - Name = name; - HueStart = hueStart; - HueCount = hueCount; + m.SendLocalizedMessage(502623); // You have no hair to dye and cannot use this + return; } - public string Name { get; } + // To prevent this from being exploited, the hue is abstracted into an internal list + var entryIndex = Math.DivRem(switches[0], 100, out var hueOffset); - public int HueStart { get; } + if (entryIndex < 0 || entryIndex >= _entries.Length) + { + return; + } - public int HueCount { get; } + var e = _entries[entryIndex]; + + if (hueOffset >= 0 && hueOffset < e.HueCount) + { + var hue = e.HueStart + hueOffset; + + m.FacialHairHue = hue; + + m.SendLocalizedMessage(501199); // You dye your hair + _specialBeardDye.Delete(); + m.PlaySound(0x4E); + } } } diff --git a/Projects/UOContent/Items/Resources/Tailor/Cotton.cs b/Projects/UOContent/Items/Resources/Tailor/Cotton.cs index 01b4712db..ea6d9da29 100644 --- a/Projects/UOContent/Items/Resources/Tailor/Cotton.cs +++ b/Projects/UOContent/Items/Resources/Tailor/Cotton.cs @@ -41,10 +41,10 @@ public partial class Cotton : Item, IDyable public virtual void OnSpun(ISpinningWheel wheel, Mobile from, int hue) { - Item item = new SpoolOfThread(6); - item.Hue = hue; - - from.AddToBackpack(item); + from.AddToBackpack(new SpoolOfThread(6) + { + Hue = hue + }); from.SendLocalizedMessage(1010577); // You put the spools of thread in your backpack. } @@ -89,4 +89,4 @@ public partial class Cotton : Item, IDyable } } } -} \ No newline at end of file +} diff --git a/Projects/UOContent/Items/Resources/Tailor/Flax.cs b/Projects/UOContent/Items/Resources/Tailor/Flax.cs index 77f684f23..464778b14 100644 --- a/Projects/UOContent/Items/Resources/Tailor/Flax.cs +++ b/Projects/UOContent/Items/Resources/Tailor/Flax.cs @@ -42,10 +42,10 @@ public partial class Flax : Item public virtual void OnSpun(ISpinningWheel wheel, Mobile from, int hue) { - Item item = new SpoolOfThread(6); - item.Hue = hue; - - from.AddToBackpack(item); + from.AddToBackpack(new SpoolOfThread(6) + { + Hue = hue + }); from.SendLocalizedMessage(1010577); // You put the spools of thread in your backpack. } diff --git a/Projects/UOContent/Items/Resources/Tailor/Wool.cs b/Projects/UOContent/Items/Resources/Tailor/Wool.cs index 0dcec170d..5320cbd8d 100644 --- a/Projects/UOContent/Items/Resources/Tailor/Wool.cs +++ b/Projects/UOContent/Items/Resources/Tailor/Wool.cs @@ -41,10 +41,10 @@ public partial class Wool : Item, IDyable public virtual void OnSpun(ISpinningWheel wheel, Mobile from, int hue) { - Item item = new DarkYarn(3); - item.Hue = hue; - - from.AddToBackpack(item); + from.AddToBackpack(new DarkYarn(3) + { + Hue = hue + }); from.SendLocalizedMessage(1010576); // You put the balls of yarn in your backpack. } @@ -105,10 +105,10 @@ public partial class TaintedWool : Wool public override void OnSpun(ISpinningWheel wheel, Mobile from, int hue) { - Item item = new DarkYarn(); - item.Hue = hue; - - from.AddToBackpack(item); + from.AddToBackpack(new DarkYarn + { + Hue = hue + }); from.SendLocalizedMessage(1010574); // You put a ball of yarn in your backpack. } -} \ No newline at end of file +} diff --git a/Projects/UOContent/Items/Resources/Tailor/YarnsAndThreads.cs b/Projects/UOContent/Items/Resources/Tailor/YarnsAndThreads.cs index 9d8e2766d..abce822ec 100644 --- a/Projects/UOContent/Items/Resources/Tailor/YarnsAndThreads.cs +++ b/Projects/UOContent/Items/Resources/Tailor/YarnsAndThreads.cs @@ -75,8 +75,10 @@ public abstract partial class BaseClothMaterial : Item, IDyable } else { - Item create = new BoltOfCloth(); - create.Hue = m_Material.Hue; + var create = new BoltOfCloth + { + Hue = m_Material.Hue + }; m_Material.Consume(); loom.Phase = 0; @@ -126,4 +128,4 @@ public partial class SpoolOfThread : BaseClothMaterial public SpoolOfThread(int amount = 1) : base(0xFA0, amount) { } -} \ No newline at end of file +} diff --git a/Projects/UOContent/Items/Skill Items/Harvest Tools/ProspectorsTool.cs b/Projects/UOContent/Items/Skill Items/Harvest Tools/ProspectorsTool.cs index 7ab9e912d..c2372223b 100644 --- a/Projects/UOContent/Items/Skill Items/Harvest Tools/ProspectorsTool.cs +++ b/Projects/UOContent/Items/Skill Items/Harvest Tools/ProspectorsTool.cs @@ -61,7 +61,7 @@ public partial class ProspectorsTool : BaseBashing, IUsesRemaining return; } - HarvestSystem system = Mining.System; + var system = Mining.System; if (!system.GetHarvestDetails(from, this, toProspect, out var tileID, out var map, out var loc, out var isLand)) { diff --git a/Projects/UOContent/Items/Special/Broken Furniture Collection/BrokenBed.cs b/Projects/UOContent/Items/Special/Broken Furniture Collection/BrokenBed.cs index d1b002002..921507f48 100644 --- a/Projects/UOContent/Items/Special/Broken Furniture Collection/BrokenBed.cs +++ b/Projects/UOContent/Items/Special/Broken Furniture Collection/BrokenBed.cs @@ -1,6 +1,5 @@ using ModernUO.Serialization; using Server.Gumps; -using Server.Network; namespace Server.Items; @@ -30,14 +29,14 @@ public partial class BrokenBedAddon : BaseAddon } [SerializationGenerator(0)] -public partial class BrokenBedDeed : BaseAddonDeed +public partial class BrokenBedDeed : BaseAddonDeed, IDirectionAddonDeed { - private bool _east; + public bool East { get; set; } [Constructible] public BrokenBedDeed() => LootType = LootType.Blessed; - public override BaseAddon Addon => new BrokenBedAddon(_east); + public override BaseAddon Addon => new BrokenBedAddon(East); public override int LabelNumber => 1076263; // Broken Bed @@ -53,49 +52,17 @@ public partial class BrokenBedDeed : BaseAddonDeed } } - private void SendTarget(Mobile m) + public void SendTarget(Mobile m) { base.OnDoubleClick(m); } - private class InternalGump : Gump + private class InternalGump : SelectAddonDirectionGump { - private readonly BrokenBedDeed _deed; - - public override bool Singleton => true; - - public InternalGump(BrokenBedDeed deed) : base(60, 36) + public InternalGump(IDirectionAddonDeed deed) : base(deed) { - _deed = deed; - - AddPage(0); - - AddBackground(0, 0, 273, 324, 0x13BE); - AddImageTiled(10, 10, 253, 20, 0xA40); - AddImageTiled(10, 40, 253, 244, 0xA40); - AddImageTiled(10, 294, 253, 20, 0xA40); - AddAlphaRegion(10, 10, 253, 304); - AddButton(10, 294, 0xFB1, 0xFB2, 0); - AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL - AddHtmlLocalized(14, 12, 273, 20, 1076749, 0x7FFF); // Please select your broken bed position - - AddPage(1); - - AddButton(19, 49, 0x845, 0x846, 1); - AddHtmlLocalized(44, 47, 213, 20, 1075386, 0x7FFF); // South - AddButton(19, 73, 0x845, 0x846, 2); - AddHtmlLocalized(44, 71, 213, 20, 1075387, 0x7FFF); // East } - public override void OnResponse(NetState sender, in RelayInfo info) - { - if (_deed?.Deleted != false || info.ButtonID == 0) - { - return; - } - - _deed._east = info.ButtonID != 1; - _deed.SendTarget(sender.Mobile); - } + public override int SelectionNumber => 1076749; // Please select your broken bed position } } diff --git a/Projects/UOContent/Items/Special/Broken Furniture Collection/BrokenVanity.cs b/Projects/UOContent/Items/Special/Broken Furniture Collection/BrokenVanity.cs index 06c0afa27..fb8402a8f 100644 --- a/Projects/UOContent/Items/Special/Broken Furniture Collection/BrokenVanity.cs +++ b/Projects/UOContent/Items/Special/Broken Furniture Collection/BrokenVanity.cs @@ -1,6 +1,5 @@ using ModernUO.Serialization; using Server.Gumps; -using Server.Network; namespace Server.Items; @@ -26,14 +25,15 @@ public partial class BrokenVanityAddon : BaseAddon } [SerializationGenerator(0)] -public partial class BrokenVanityDeed : BaseAddonDeed +public partial class BrokenVanityDeed : BaseAddonDeed, IDirectionAddonDeed { - private bool _east; + public bool East { get; set; } [Constructible] public BrokenVanityDeed() => LootType = LootType.Blessed; - public override BaseAddon Addon => new BrokenVanityAddon(_east); + public override BaseAddon Addon => new BrokenVanityAddon(East); + public override int LabelNumber => 1076260; // Broken Vanity public override void OnDoubleClick(Mobile from) @@ -48,49 +48,17 @@ public partial class BrokenVanityDeed : BaseAddonDeed } } - private void SendTarget(Mobile m) + public void SendTarget(Mobile m) { base.OnDoubleClick(m); } - private class InternalGump : Gump + private class InternalGump : SelectAddonDirectionGump { - private readonly BrokenVanityDeed _deed; - - public override bool Singleton => true; - - public InternalGump(BrokenVanityDeed deed) : base(60, 63) + public InternalGump(IDirectionAddonDeed deed) : base(deed) { - _deed = deed; - - AddPage(0); - - AddBackground(0, 0, 273, 324, 0x13BE); - AddImageTiled(10, 10, 253, 20, 0xA40); - AddImageTiled(10, 40, 253, 244, 0xA40); - AddImageTiled(10, 294, 253, 20, 0xA40); - AddAlphaRegion(10, 10, 253, 304); - AddButton(10, 294, 0xFB1, 0xFB2, 0); - AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL - AddHtmlLocalized(14, 12, 273, 20, 1076747, 0x7FFF); // Please select your broken vanity position - - AddPage(1); - - AddButton(19, 49, 0x845, 0x846, 1); - AddHtmlLocalized(44, 47, 213, 20, 1075386, 0x7FFF); // South - AddButton(19, 73, 0x845, 0x846, 2); - AddHtmlLocalized(44, 71, 213, 20, 1075387, 0x7FFF); // East } - public override void OnResponse(NetState sender, in RelayInfo info) - { - if (_deed?.Deleted != false || info.ButtonID == 0) - { - return; - } - - _deed._east = info.ButtonID != 1; - _deed.SendTarget(sender.Mobile); - } + public override int SelectionNumber => 1076747; // Please select your broken vanity position } } diff --git a/Projects/UOContent/Items/Special/Gifts/HearthOfHomeFire.cs b/Projects/UOContent/Items/Special/Gifts/HearthOfHomeFire.cs index a2c64d180..6ba993842 100644 --- a/Projects/UOContent/Items/Special/Gifts/HearthOfHomeFire.cs +++ b/Projects/UOContent/Items/Special/Gifts/HearthOfHomeFire.cs @@ -60,25 +60,27 @@ public partial class HearthOfHomeFireDeed : BaseAddonDeed base.OnDoubleClick(m); } - private class InternalGump : Gump + private class InternalGump : StaticGump { private readonly HearthOfHomeFireDeed _deed; public override bool Singleton => true; - public InternalGump(HearthOfHomeFireDeed deed) : base(150, 50) + public InternalGump(HearthOfHomeFireDeed deed) : base(150, 50) => _deed = deed; + + protected override void BuildLayout(ref StaticGumpBuilder builder) { - _deed = deed; + builder.AddBackground(0, 0, 350, 250, 0xA28); - AddBackground(0, 0, 350, 250, 0xA28); + builder.AddPage(); - AddItem(90, 52, 0x2367); - AddItem(112, 35, 0x2360); - AddButton(70, 35, 0x868, 0x869, 1); // South + builder.AddItem(90, 52, 0x2367); + builder.AddItem(112, 35, 0x2360); + builder.AddButton(70, 35, 0x868, 0x869, 1); // South - AddItem(220, 35, 0x2352); - AddItem(242, 52, 0x2358); - AddButton(185, 35, 0x868, 0x869, 2); // East + builder.AddItem(220, 35, 0x2352); + builder.AddItem(242, 52, 0x2358); + builder.AddButton(185, 35, 0x868, 0x869, 2); // East } public override void OnResponse(NetState sender, in RelayInfo info) diff --git a/Projects/UOContent/Items/Special/Heritage Items/Curtains.cs b/Projects/UOContent/Items/Special/Heritage Items/Curtains.cs index 4f1b2347e..d1621b4e2 100644 --- a/Projects/UOContent/Items/Special/Heritage Items/Curtains.cs +++ b/Projects/UOContent/Items/Special/Heritage Items/Curtains.cs @@ -1,6 +1,5 @@ using ModernUO.Serialization; using Server.Gumps; -using Server.Network; namespace Server.Items; @@ -77,14 +76,14 @@ public partial class CurtainsAddon : BaseAddon } [SerializationGenerator(0)] -public partial class CurtainsDeed : BaseAddonDeed +public partial class CurtainsDeed : BaseAddonDeed, IDirectionAddonDeed { - private bool _east; + public bool East { get; set; } [Constructible] public CurtainsDeed() => LootType = LootType.Blessed; - public override BaseAddon Addon => new CurtainsAddon(_east); + public override BaseAddon Addon => new CurtainsAddon(East); public override int LabelNumber => 1076280; // Curtains public override void OnDoubleClick(Mobile from) @@ -99,49 +98,17 @@ public partial class CurtainsDeed : BaseAddonDeed } } - private void SendTarget(Mobile m) + public void SendTarget(Mobile m) { base.OnDoubleClick(m); } - private class InternalGump : Gump + private class InternalGump : SelectAddonDirectionGump { - private readonly CurtainsDeed _deed; - - public override bool Singleton => true; - - public InternalGump(CurtainsDeed deed) : base(60, 36) + public InternalGump(IDirectionAddonDeed deed) : base(deed) { - _deed = deed; - - AddPage(0); - - AddBackground(0, 0, 273, 324, 0x13BE); - AddImageTiled(10, 10, 253, 20, 0xA40); - AddImageTiled(10, 40, 253, 244, 0xA40); - AddImageTiled(10, 294, 253, 20, 0xA40); - AddAlphaRegion(10, 10, 253, 304); - AddButton(10, 294, 0xFB1, 0xFB2, 0); - AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL - AddHtmlLocalized(14, 12, 273, 20, 1076581, 0x7FFF); // Please select your curtain position - - AddPage(1); - - AddButton(19, 49, 0x845, 0x846, 1); - AddHtmlLocalized(44, 47, 213, 20, 1075386, 0x7FFF); // South - AddButton(19, 73, 0x845, 0x846, 2); - AddHtmlLocalized(44, 71, 213, 20, 1075387, 0x7FFF); // East } - public override void OnResponse(NetState sender, in RelayInfo info) - { - if (_deed?.Deleted != false || info.ButtonID == 0) - { - return; - } - - _deed._east = info.ButtonID != 1; - _deed.SendTarget(sender.Mobile); - } + public override int SelectionNumber => 1076581; // Please select your curtain position } } diff --git a/Projects/UOContent/Items/Special/Heritage Items/HangingAxes.cs b/Projects/UOContent/Items/Special/Heritage Items/HangingAxes.cs index ce82e6ec2..d51757a3e 100644 --- a/Projects/UOContent/Items/Special/Heritage Items/HangingAxes.cs +++ b/Projects/UOContent/Items/Special/Heritage Items/HangingAxes.cs @@ -1,6 +1,5 @@ using ModernUO.Serialization; using Server.Gumps; -using Server.Network; namespace Server.Items; @@ -26,14 +25,14 @@ public partial class HangingAxesAddon : BaseAddon } [SerializationGenerator(0)] -public partial class HangingAxesDeed : BaseAddonDeed +public partial class HangingAxesDeed : BaseAddonDeed, IDirectionAddonDeed { - private bool m_East; + public bool East { get; set; } [Constructible] public HangingAxesDeed() => LootType = LootType.Blessed; - public override BaseAddon Addon => new HangingAxesAddon(m_East); + public override BaseAddon Addon => new HangingAxesAddon(East); public override int LabelNumber => 1076271; // Hanging Axes public override void OnDoubleClick(Mobile from) @@ -48,49 +47,17 @@ public partial class HangingAxesDeed : BaseAddonDeed } } - private void SendTarget(Mobile m) + public void SendTarget(Mobile m) { base.OnDoubleClick(m); } - private class InternalGump : Gump + private class InternalGump : SelectAddonDirectionGump { - private readonly HangingAxesDeed m_Deed; - - public override bool Singleton => true; - - public InternalGump(HangingAxesDeed deed) : base(60, 36) + public InternalGump(IDirectionAddonDeed deed) : base(deed) { - m_Deed = deed; - - AddPage(0); - - AddBackground(0, 0, 273, 324, 0x13BE); - AddImageTiled(10, 10, 253, 20, 0xA40); - AddImageTiled(10, 40, 253, 244, 0xA40); - AddImageTiled(10, 294, 253, 20, 0xA40); - AddAlphaRegion(10, 10, 253, 304); - AddButton(10, 294, 0xFB1, 0xFB2, 0); - AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL - AddHtmlLocalized(14, 12, 273, 20, 1076745, 0x7FFF); // Please select your hanging axe position - - AddPage(1); - - AddButton(19, 49, 0x845, 0x846, 1); - AddHtmlLocalized(44, 47, 213, 20, 1075386, 0x7FFF); // South - AddButton(19, 73, 0x845, 0x846, 2); - AddHtmlLocalized(44, 71, 213, 20, 1075387, 0x7FFF); // East } - public override void OnResponse(NetState sender, in RelayInfo info) - { - if (m_Deed?.Deleted != false || info.ButtonID == 0) - { - return; - } - - m_Deed.m_East = info.ButtonID != 1; - m_Deed.SendTarget(sender.Mobile); - } + public override int SelectionNumber => 1076745; // Please select your hanging axe position } } diff --git a/Projects/UOContent/Items/Special/Heritage Items/HangingSwords.cs b/Projects/UOContent/Items/Special/Heritage Items/HangingSwords.cs index 3d23fc936..1a251720a 100644 --- a/Projects/UOContent/Items/Special/Heritage Items/HangingSwords.cs +++ b/Projects/UOContent/Items/Special/Heritage Items/HangingSwords.cs @@ -1,6 +1,5 @@ using ModernUO.Serialization; using Server.Gumps; -using Server.Network; namespace Server.Items; @@ -26,14 +25,14 @@ public partial class HangingSwordsAddon : BaseAddon } [SerializationGenerator(0)] -public partial class HangingSwordsDeed : BaseAddonDeed +public partial class HangingSwordsDeed : BaseAddonDeed, IDirectionAddonDeed { - private bool _east; + public bool East { get; set; } [Constructible] public HangingSwordsDeed() => LootType = LootType.Blessed; - public override BaseAddon Addon => new HangingSwordsAddon(_east); + public override BaseAddon Addon => new HangingSwordsAddon(East); public override int LabelNumber => 1076272; // Hanging Swords public override void OnDoubleClick(Mobile from) @@ -48,49 +47,17 @@ public partial class HangingSwordsDeed : BaseAddonDeed } } - private void SendTarget(Mobile m) + public void SendTarget(Mobile m) { base.OnDoubleClick(m); } - private class InternalGump : Gump + private class InternalGump : SelectAddonDirectionGump { - private readonly HangingSwordsDeed m_Deed; - - public override bool Singleton => true; - - public InternalGump(HangingSwordsDeed deed) : base(60, 36) + public InternalGump(IDirectionAddonDeed deed) : base(deed) { - m_Deed = deed; - - AddPage(0); - - AddBackground(0, 0, 273, 324, 0x13BE); - AddImageTiled(10, 10, 253, 20, 0xA40); - AddImageTiled(10, 40, 253, 244, 0xA40); - AddImageTiled(10, 294, 253, 20, 0xA40); - AddAlphaRegion(10, 10, 253, 304); - AddButton(10, 294, 0xFB1, 0xFB2, 0); - AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL - AddHtmlLocalized(14, 12, 273, 20, 1076746, 0x7FFF); // Please select your hanging sword position - - AddPage(1); - - AddButton(19, 49, 0x845, 0x846, 1); - AddHtmlLocalized(44, 47, 213, 20, 1075386, 0x7FFF); // South - AddButton(19, 73, 0x845, 0x846, 2); - AddHtmlLocalized(44, 71, 213, 20, 1075387, 0x7FFF); // East } - public override void OnResponse(NetState sender, in RelayInfo info) - { - if (m_Deed?.Deleted != false || info.ButtonID == 0) - { - return; - } - - m_Deed._east = info.ButtonID != 1; - m_Deed.SendTarget(sender.Mobile); - } + public override int SelectionNumber => 1076746; // Please select your hanging sword position } } diff --git a/Projects/UOContent/Items/Special/Heritage Items/HouseLadder.cs b/Projects/UOContent/Items/Special/Heritage Items/HouseLadder.cs index f1a36692e..19cc0b684 100644 --- a/Projects/UOContent/Items/Special/Heritage Items/HouseLadder.cs +++ b/Projects/UOContent/Items/Special/Heritage Items/HouseLadder.cs @@ -94,46 +94,47 @@ public partial class HouseLadderDeed : BaseAddonDeed base.OnDoubleClick(m); } - private class InternalGump : Gump + private class InternalGump : StaticGump { private readonly HouseLadderDeed _deed; public override bool Singleton => true; - public InternalGump(HouseLadderDeed deed) : base(60, 36) + public InternalGump(HouseLadderDeed deed) : base(60, 36) => _deed = deed; + + protected override void BuildLayout(ref StaticGumpBuilder builder) { - _deed = deed; + builder.AddPage(); - AddPage(0); + builder.AddBackground(0, 0, 273, 324, 0x13BE); + builder.AddImageTiled(10, 10, 253, 20, 0xA40); + builder.AddImageTiled(10, 40, 253, 244, 0xA40); + builder.AddImageTiled(10, 294, 253, 20, 0xA40); + builder.AddAlphaRegion(10, 10, 253, 304); + builder.AddButton(10, 294, 0xFB1, 0xFB2, 0); + builder.AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL - AddBackground(0, 0, 273, 324, 0x13BE); - AddImageTiled(10, 10, 253, 20, 0xA40); - AddImageTiled(10, 40, 253, 244, 0xA40); - AddImageTiled(10, 294, 253, 20, 0xA40); - AddAlphaRegion(10, 10, 253, 304); - AddButton(10, 294, 0xFB1, 0xFB2, 0); - AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL // Please select your ladder position.
Use the ladders marked (castle)
for accessing the tops of keeps
and castles. - AddHtmlLocalized(14, 12, 273, 20, 1076780, 0x7FFF); + builder.AddHtmlLocalized(14, 12, 273, 20, 1076780, 0x7FFF); - AddPage(1); + builder.AddPage(1); - AddButton(19, 49, 0x845, 0x846, 1); - AddHtmlLocalized(44, 47, 213, 20, 1076794, 0x7FFF); // South (Castle) - AddButton(19, 73, 0x845, 0x846, 2); - AddHtmlLocalized(44, 71, 213, 20, 1076795, 0x7FFF); // East (Castle) - AddButton(19, 97, 0x845, 0x846, 3); - AddHtmlLocalized(44, 95, 213, 20, 1076792, 0x7FFF); // North (Castle) - AddButton(19, 121, 0x845, 0x846, 4); - AddHtmlLocalized(44, 119, 213, 20, 1076793, 0x7FFF); // West (Castle) - AddButton(19, 145, 0x845, 0x846, 5); - AddHtmlLocalized(44, 143, 213, 20, 1075386, 0x7FFF); // South - AddButton(19, 169, 0x845, 0x846, 6); - AddHtmlLocalized(44, 167, 213, 20, 1075387, 0x7FFF); // East - AddButton(19, 193, 0x845, 0x846, 7); - AddHtmlLocalized(44, 191, 213, 20, 1075389, 0x7FFF); // North - AddButton(19, 217, 0x845, 0x846, 8); - AddHtmlLocalized(44, 215, 213, 20, 1075390, 0x7FFF); // West + builder.AddButton(19, 49, 0x845, 0x846, 1); + builder.AddHtmlLocalized(44, 47, 213, 20, 1076794, 0x7FFF); // South (Castle) + builder.AddButton(19, 73, 0x845, 0x846, 2); + builder.AddHtmlLocalized(44, 71, 213, 20, 1076795, 0x7FFF); // East (Castle) + builder.AddButton(19, 97, 0x845, 0x846, 3); + builder.AddHtmlLocalized(44, 95, 213, 20, 1076792, 0x7FFF); // North (Castle) + builder.AddButton(19, 121, 0x845, 0x846, 4); + builder.AddHtmlLocalized(44, 119, 213, 20, 1076793, 0x7FFF); // West (Castle) + builder.AddButton(19, 145, 0x845, 0x846, 5); + builder.AddHtmlLocalized(44, 143, 213, 20, 1075386, 0x7FFF); // South + builder.AddButton(19, 169, 0x845, 0x846, 6); + builder.AddHtmlLocalized(44, 167, 213, 20, 1075387, 0x7FFF); // East + builder.AddButton(19, 193, 0x845, 0x846, 7); + builder.AddHtmlLocalized(44, 191, 213, 20, 1075389, 0x7FFF); // North + builder.AddButton(19, 217, 0x845, 0x846, 8); + builder.AddHtmlLocalized(44, 215, 213, 20, 1075390, 0x7FFF); // West } public override void OnResponse(NetState sender, in RelayInfo info) diff --git a/Projects/UOContent/Items/Special/Heritage Items/Statue.cs b/Projects/UOContent/Items/Special/Heritage Items/Statue.cs index a07b7980f..3ef76af94 100644 --- a/Projects/UOContent/Items/Special/Heritage Items/Statue.cs +++ b/Projects/UOContent/Items/Special/Heritage Items/Statue.cs @@ -1,6 +1,5 @@ using ModernUO.Serialization; using Server.Gumps; -using Server.Network; namespace Server.Items; @@ -28,14 +27,14 @@ public partial class StoneStatueAddon : BaseAddon } [SerializationGenerator(0)] -public partial class StoneStatueDeed : BaseAddonDeed +public partial class StoneStatueDeed : BaseAddonDeed, IDirectionAddonDeed { - private bool _east; + public bool East { get; set; } [Constructible] public StoneStatueDeed() => LootType = LootType.Blessed; - public override BaseAddon Addon => new StoneStatueAddon(_east); + public override BaseAddon Addon => new StoneStatueAddon(East); public override int LabelNumber => 1076284; // Statue public override void OnDoubleClick(Mobile from) @@ -50,49 +49,17 @@ public partial class StoneStatueDeed : BaseAddonDeed } } - private void SendTarget(Mobile m) + public void SendTarget(Mobile m) { base.OnDoubleClick(m); } - private class InternalGump : Gump + private class InternalGump : SelectAddonDirectionGump { - private readonly StoneStatueDeed _deed; - - public override bool Singleton => true; - - public InternalGump(StoneStatueDeed deed) : base(60, 36) + public InternalGump(IDirectionAddonDeed deed) : base(deed) { - _deed = deed; - - AddPage(0); - - AddBackground(0, 0, 273, 324, 0x13BE); - AddImageTiled(10, 10, 253, 20, 0xA40); - AddImageTiled(10, 40, 253, 244, 0xA40); - AddImageTiled(10, 294, 253, 20, 0xA40); - AddAlphaRegion(10, 10, 253, 304); - AddButton(10, 294, 0xFB1, 0xFB2, 0); - AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL - AddHtmlLocalized(14, 12, 273, 20, 1076579, 0x7FFF); // Please select your statue position - - AddPage(1); - - AddButton(19, 49, 0x845, 0x846, 1); - AddHtmlLocalized(44, 47, 213, 20, 1075386, 0x7FFF); // South - AddButton(19, 73, 0x845, 0x846, 2); - AddHtmlLocalized(44, 71, 213, 20, 1075387, 0x7FFF); // East } - public override void OnResponse(NetState sender, in RelayInfo info) - { - if (_deed?.Deleted != false || info.ButtonID == 0) - { - return; - } - - _deed._east = info.ButtonID != 1; - _deed.SendTarget(sender.Mobile); - } + public override int SelectionNumber => 1076579; // Please select your statue position } } diff --git a/Projects/UOContent/Items/Special/Heritage Items/UnmadeBed.cs b/Projects/UOContent/Items/Special/Heritage Items/UnmadeBed.cs index ca28c4312..2fa5b5b6f 100644 --- a/Projects/UOContent/Items/Special/Heritage Items/UnmadeBed.cs +++ b/Projects/UOContent/Items/Special/Heritage Items/UnmadeBed.cs @@ -1,6 +1,5 @@ using ModernUO.Serialization; using Server.Gumps; -using Server.Network; namespace Server.Items; @@ -30,14 +29,14 @@ public partial class UnmadeBedAddon : BaseAddon } [SerializationGenerator(0)] -public partial class UnmadeBedDeed : BaseAddonDeed +public partial class UnmadeBedDeed : BaseAddonDeed, IDirectionAddonDeed { - private bool _east; + public bool East { get; set; } [Constructible] public UnmadeBedDeed() => LootType = LootType.Blessed; - public override BaseAddon Addon => new UnmadeBedAddon(_east); + public override BaseAddon Addon => new UnmadeBedAddon(East); public override int LabelNumber => 1076279; // Unmade Bed public override void OnDoubleClick(Mobile from) @@ -52,49 +51,18 @@ public partial class UnmadeBedDeed : BaseAddonDeed } } - private void SendTarget(Mobile m) + public void SendTarget(Mobile m) { base.OnDoubleClick(m); } - private class InternalGump : Gump + private class InternalGump : SelectAddonDirectionGump { - private readonly UnmadeBedDeed _deed; - - public override bool Singleton => true; - - public InternalGump(UnmadeBedDeed deed) : base(60, 36) + public InternalGump(IDirectionAddonDeed deed) : base(deed) { - _deed = deed; - - AddPage(0); - - AddBackground(0, 0, 273, 324, 0x13BE); - AddImageTiled(10, 10, 253, 20, 0xA40); - AddImageTiled(10, 40, 253, 244, 0xA40); - AddImageTiled(10, 294, 253, 20, 0xA40); - AddAlphaRegion(10, 10, 253, 304); - AddButton(10, 294, 0xFB1, 0xFB2, 0); - AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL - AddHtmlLocalized(14, 12, 273, 20, 1076580, 0x7FFF); // Please select your unmade bed position - - AddPage(1); - - AddButton(19, 49, 0x845, 0x846, 1); - AddHtmlLocalized(44, 47, 213, 20, 1075386, 0x7FFF); // South - AddButton(19, 73, 0x845, 0x846, 2); - AddHtmlLocalized(44, 71, 213, 20, 1075387, 0x7FFF); // East } - public override void OnResponse(NetState sender, in RelayInfo info) - { - if (_deed?.Deleted != false || info.ButtonID == 0) - { - return; - } - - _deed._east = info.ButtonID != 1; - _deed.SendTarget(sender.Mobile); - } + // Misspelled in cliloc + public override int SelectionNumber => 1076580; // Pleae select your unmade bed position } } diff --git a/Projects/UOContent/Items/Special/Heritage Items/Vanity.cs b/Projects/UOContent/Items/Special/Heritage Items/Vanity.cs index 6dfcc3fff..e001ecd31 100644 --- a/Projects/UOContent/Items/Special/Heritage Items/Vanity.cs +++ b/Projects/UOContent/Items/Special/Heritage Items/Vanity.cs @@ -1,6 +1,5 @@ using ModernUO.Serialization; using Server.Gumps; -using Server.Network; namespace Server.Items; @@ -27,14 +26,14 @@ public partial class VanityAddon : BaseAddonContainer } [SerializationGenerator(0)] -public partial class VanityDeed : BaseAddonContainerDeed +public partial class VanityDeed : BaseAddonContainerDeed, IDirectionAddonDeed { - private bool m_East; + public bool East { get; set; } [Constructible] public VanityDeed() => LootType = LootType.Blessed; - public override BaseAddonContainer Addon => new VanityAddon(m_East); + public override BaseAddonContainer Addon => new VanityAddon(East); public override int LabelNumber => 1074027; // Vanity public override void OnDoubleClick(Mobile from) @@ -49,49 +48,17 @@ public partial class VanityDeed : BaseAddonContainerDeed } } - private void SendTarget(Mobile m) + public void SendTarget(Mobile m) { base.OnDoubleClick(m); } - private class InternalGump : Gump + private class InternalGump : SelectAddonDirectionGump { - private readonly VanityDeed _deed; - - public override bool Singleton => true; - - public InternalGump(VanityDeed deed) : base(60, 36) + public InternalGump(IDirectionAddonDeed deed) : base(deed) { - _deed = deed; - - AddPage(0); - - AddBackground(0, 0, 273, 324, 0x13BE); - AddImageTiled(10, 10, 253, 20, 0xA40); - AddImageTiled(10, 40, 253, 244, 0xA40); - AddImageTiled(10, 294, 253, 20, 0xA40); - AddAlphaRegion(10, 10, 253, 304); - AddButton(10, 294, 0xFB1, 0xFB2, 0); - AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL - AddHtmlLocalized(14, 12, 273, 20, 1076744, 0x7FFF); // Please select your vanity position. - - AddPage(1); - - AddButton(19, 49, 0x845, 0x846, 1); - AddHtmlLocalized(44, 47, 213, 20, 1075386, 0x7FFF); // South - AddButton(19, 73, 0x845, 0x846, 2); - AddHtmlLocalized(44, 71, 213, 20, 1075387, 0x7FFF); // East } - public override void OnResponse(NetState sender, in RelayInfo info) - { - if (_deed?.Deleted != false || info.ButtonID == 0) - { - return; - } - - _deed.m_East = info.ButtonID != 1; - _deed.SendTarget(sender.Mobile); - } + public override int SelectionNumber => 1076744; // Please select your vanity position. } } diff --git a/Projects/UOContent/Items/Special/Heritage Items/WoodenCoffin.cs b/Projects/UOContent/Items/Special/Heritage Items/WoodenCoffin.cs index b6cf3be6b..cfbf8129e 100644 --- a/Projects/UOContent/Items/Special/Heritage Items/WoodenCoffin.cs +++ b/Projects/UOContent/Items/Special/Heritage Items/WoodenCoffin.cs @@ -1,6 +1,5 @@ using ModernUO.Serialization; using Server.Gumps; -using Server.Network; namespace Server.Items; @@ -38,14 +37,14 @@ public partial class WoodenCoffinAddon : BaseAddon } [SerializationGenerator(0)] -public partial class WoodenCoffinDeed : BaseAddonDeed +public partial class WoodenCoffinDeed : BaseAddonDeed, IDirectionAddonDeed { - private bool m_East; + public bool East { get; set; } [Constructible] public WoodenCoffinDeed() => LootType = LootType.Blessed; - public override BaseAddon Addon => new WoodenCoffinAddon(m_East); + public override BaseAddon Addon => new WoodenCoffinAddon(East); public override int LabelNumber => 1076274; // Coffin public override void OnDoubleClick(Mobile from) @@ -60,49 +59,17 @@ public partial class WoodenCoffinDeed : BaseAddonDeed } } - private void SendTarget(Mobile m) + public void SendTarget(Mobile m) { base.OnDoubleClick(m); } - private class InternalGump : Gump + private class InternalGump : SelectAddonDirectionGump { - private readonly WoodenCoffinDeed _deed; - - public override bool Singleton => true; - - public InternalGump(WoodenCoffinDeed deed) : base(60, 36) + public InternalGump(IDirectionAddonDeed deed) : base(deed) { - _deed = deed; - - AddPage(0); - - AddBackground(0, 0, 273, 324, 0x13BE); - AddImageTiled(10, 10, 253, 20, 0xA40); - AddImageTiled(10, 40, 253, 244, 0xA40); - AddImageTiled(10, 294, 253, 20, 0xA40); - AddAlphaRegion(10, 10, 253, 304); - AddButton(10, 294, 0xFB1, 0xFB2, 0); - AddHtmlLocalized(45, 296, 450, 20, 1060051, 0x7FFF); // CANCEL - AddHtmlLocalized(14, 12, 273, 20, 1076748, 0x7FFF); // Please select your coffin position - - AddPage(1); - - AddButton(19, 49, 0x845, 0x846, 1); - AddHtmlLocalized(44, 47, 213, 20, 1075386, 0x7FFF); // South - AddButton(19, 73, 0x845, 0x846, 2); - AddHtmlLocalized(44, 71, 213, 20, 1075387, 0x7FFF); // East } - public override void OnResponse(NetState sender, in RelayInfo info) - { - if (_deed?.Deleted != false || info.ButtonID == 0) - { - return; - } - - _deed.m_East = info.ButtonID != 1; - _deed.SendTarget(sender.Mobile); - } + public override int SelectionNumber => 1076748; // Please select your coffin position } } diff --git a/Projects/UOContent/Items/Special/Holiday/Wreath.cs b/Projects/UOContent/Items/Special/Holiday/Wreath.cs index 37d844bfc..0d0333017 100644 --- a/Projects/UOContent/Items/Special/Holiday/Wreath.cs +++ b/Projects/UOContent/Items/Special/Holiday/Wreath.cs @@ -212,7 +212,7 @@ public partial class WreathDeed : Item if (northWall && westWall) { - from.SendGump(new WreathDeedGump(from, loc, this)); + from.SendGump(new WreathDeedGump(loc, this)); } else { @@ -257,57 +257,56 @@ public partial class WreathDeed : Item if (itemID > 0) { - Item addon = new WreathAddon(Hue); + var addon = new WreathAddon(Hue) + { + ItemID = itemID + }; - addon.ItemID = itemID; addon.MoveToWorld(loc, from.Map); - house.Addons.Add(addon); Delete(); } } - private class WreathDeedGump : Gump + private class WreathDeedGump : StaticGump { private readonly WreathDeed _deed; - private readonly Mobile _from; private readonly Point3D _loc; - public WreathDeedGump(Mobile from, Point3D loc, WreathDeed deed) : base(150, 50) + public WreathDeedGump(Point3D loc, WreathDeed deed) : base(150, 50) { - _from = from; _loc = loc; _deed = deed; + } - AddBackground(0, 0, 300, 150, 0xA28); + protected override void BuildLayout(ref StaticGumpBuilder builder) + { + builder.AddBackground(0, 0, 300, 150, 0xA28); - AddPage(0); + builder.AddPage(); - AddItem(90, 30, 0x232D); - AddItem(180, 30, 0x232C); - AddButton(50, 35, 0x868, 0x869, 1); - AddButton(145, 35, 0x868, 0x869, 2); + builder.AddItem(90, 30, 0x232D); + builder.AddItem(180, 30, 0x232C); + builder.AddButton(50, 35, 0x868, 0x869, 1); + builder.AddButton(145, 35, 0x868, 0x869, 2); } public override void OnResponse(NetState sender, in RelayInfo info) { - if (_deed.Deleted) + if (_deed.Deleted || info.ButtonID == 0) { return; } - switch (info.ButtonID) + var from = sender.Mobile; + + if (info.ButtonID == 1) { - case 1: - { - _deed.PlaceAddon(_from, _loc, false, true); - break; - } - case 2: - { - _deed.PlaceAddon(_from, _loc, true, false); - break; - } + _deed.PlaceAddon(from, _loc, false, true); + } + else + { + _deed.PlaceAddon(from, _loc, true, false); } } } diff --git a/Projects/UOContent/Items/Special/Mutation Core/PlagueBeastBackpack.cs b/Projects/UOContent/Items/Special/Mutation Core/PlagueBeastBackpack.cs index f9681165c..5aeeefa2c 100644 --- a/Projects/UOContent/Items/Special/Mutation Core/PlagueBeastBackpack.cs +++ b/Projects/UOContent/Items/Special/Mutation Core/PlagueBeastBackpack.cs @@ -125,7 +125,7 @@ public partial class PlagueBeastBackpack : BaseContainer return false; } - var ir = ItemBounds.Table[item.ItemID]; + var ir = ItemBounds.Bounds[item.ItemID]; int x, y; var cx = p.X + ir.X + ir.Width / 2; var cy = p.Y + ir.Y + ir.Height / 2; @@ -134,7 +134,7 @@ public partial class PlagueBeastBackpack : BaseContainer { if (Items[i] is PlagueBeastComponent innard) { - var r = ItemBounds.Table[innard.ItemID]; + var r = ItemBounds.Bounds[innard.ItemID]; x = innard.X + r.X; y = innard.Y + r.Y; diff --git a/Projects/UOContent/Items/Special/Veteran Rewards/Banner.cs b/Projects/UOContent/Items/Special/Veteran Rewards/Banner.cs index bb610754d..09cca1a32 100644 --- a/Projects/UOContent/Items/Special/Veteran Rewards/Banner.cs +++ b/Projects/UOContent/Items/Special/Veteran Rewards/Banner.cs @@ -292,65 +292,52 @@ public partial class BannerDeed : Item, IRewardItem } } - private class FacingGump : Gump + private class FacingGump : DynamicGump { - private readonly BannerDeed m_Banner; - private readonly BaseHouse m_House; - private readonly int m_ItemID; - private readonly Point3D m_Location; + private readonly BannerDeed _banner; + private readonly BaseHouse _house; + private readonly int _itemId; + private readonly Point3D _location; public override bool Singleton => true; public FacingGump(BannerDeed banner, int itemID, Point3D location, BaseHouse house) : base(150, 50) { - m_Banner = banner; - m_ItemID = itemID; - m_Location = location; - m_House = house; + _banner = banner; + _itemId = itemID; + _location = location; + _house = house; + } - Closable = true; - Disposable = true; - Draggable = true; - Resizable = false; + protected override void BuildLayout(ref DynamicGumpBuilder builder) + { + builder.SetNoResize(); - AddPage(0); + builder.AddPage(); - AddBackground(0, 0, 300, 150, 0xA28); + builder.AddBackground(0, 0, 300, 150, 0xA28); - AddItem(90, 30, itemID + 1); - AddItem(180, 30, itemID); + builder.AddItem(90, 30, _itemId + 1); + builder.AddItem(180, 30, _itemId); - AddButton(50, 35, 0x868, 0x869, (int)Buttons.East); - AddButton(145, 35, 0x868, 0x869, (int)Buttons.South); + builder.AddButton(50, 35, 0x868, 0x869, (int)Buttons.East); + builder.AddButton(145, 35, 0x868, 0x869, (int)Buttons.South); } public override void OnResponse(NetState sender, in RelayInfo info) { - if (m_Banner?.Deleted != false || m_House == null) + if (_banner?.Deleted != false || _house == null) { return; } - Banner banner = null; + Banner banner = new Banner(_itemId + (info.ButtonID == (int)Buttons.East ? 1 : 0)); + _house.Addons.Add(banner); - if (info.ButtonID == (int)Buttons.East) - { - banner = new Banner(m_ItemID + 1); - } - else if (info.ButtonID == (int)Buttons.South) - { - banner = new Banner(m_ItemID); - } + banner.IsRewardItem = _banner.IsRewardItem; + banner.MoveToWorld(_location, sender.Mobile.Map); - if (banner != null) - { - m_House.Addons.Add(banner); - - banner.IsRewardItem = m_Banner.IsRewardItem; - banner.MoveToWorld(m_Location, sender.Mobile.Map); - - m_Banner.Delete(); - } + _banner.Delete(); } private enum Buttons diff --git a/Projects/UOContent/Items/Special/Veteran Rewards/HangingSkeleton.cs b/Projects/UOContent/Items/Special/Veteran Rewards/HangingSkeleton.cs index a60dfe642..29dca292e 100644 --- a/Projects/UOContent/Items/Special/Veteran Rewards/HangingSkeleton.cs +++ b/Projects/UOContent/Items/Special/Veteran Rewards/HangingSkeleton.cs @@ -271,7 +271,7 @@ public partial class HangingSkeletonDeed : Item, IRewardItem } } - private class FacingGump : Gump + private class FacingGump : DynamicGump { private readonly BaseHouse _house; private readonly int _itemID; @@ -286,26 +286,26 @@ public partial class HangingSkeletonDeed : Item, IRewardItem _itemID = itemID; _location = location; _house = house; + } - Closable = true; - Disposable = true; - Draggable = true; - Resizable = false; + protected override void BuildLayout(ref DynamicGumpBuilder builder) + { + builder.SetNoResize(); - AddPage(0); + builder.AddPage(); - AddBackground(0, 0, 300, 150, 0xA28); + builder.AddBackground(0, 0, 300, 150, 0xA28); - AddItem(90, 30, GetWestItemID(itemID)); - AddItem(180, 30, itemID); + builder.AddItem(90, 30, GetWestItemID(_itemID)); + builder.AddItem(180, 30, _itemID); - AddButton(50, 35, 0x868, 0x869, (int)Buttons.East); - AddButton(145, 35, 0x868, 0x869, (int)Buttons.South); + builder.AddButton(50, 35, 0x868, 0x869, (int)Buttons.East); + builder.AddButton(145, 35, 0x868, 0x869, (int)Buttons.South); } public override void OnResponse(NetState sender, in RelayInfo info) { - if (_skeleton?.Deleted != false || _house == null) + if (_skeleton?.Deleted != false || _house == null || info.ButtonID == (int)Buttons.Cancel) { return; } @@ -316,8 +316,7 @@ public partial class HangingSkeletonDeed : Item, IRewardItem { banner = new HangingSkeleton(GetWestItemID(_itemID)); } - - if (info.ButtonID == (int)Buttons.South) + else if (info.ButtonID == (int)Buttons.South) { banner = new HangingSkeleton(_itemID); } diff --git a/Projects/UOContent/Items/Talismans/BaseTalisman.cs b/Projects/UOContent/Items/Talismans/BaseTalisman.cs index 9c659f525..67cd8b9a5 100644 --- a/Projects/UOContent/Items/Talismans/BaseTalisman.cs +++ b/Projects/UOContent/Items/Talismans/BaseTalisman.cs @@ -260,8 +260,7 @@ public partial class BaseTalisman : Item, IAosItem [SerializableFieldSaveFlag(14)] public bool ShouldSerializeSlayer() => _slayer != TalismanSlayerName.None; - private Mobile _creature; - + private BaseCreature _creature; private TimerExecutionToken _timerToken; diff --git a/Projects/UOContent/Items/Traps/FlameSpurtTrap.cs b/Projects/UOContent/Items/Traps/FlameSpurtTrap.cs index 31bff1636..61e30046c 100644 --- a/Projects/UOContent/Items/Traps/FlameSpurtTrap.cs +++ b/Projects/UOContent/Items/Traps/FlameSpurtTrap.cs @@ -8,7 +8,8 @@ namespace Server.Items; public partial class FlameSpurtTrap : BaseTrap { [SerializableField(0)] - private Item _spurt; + private Static _spurt; + private TimerExecutionToken _timerToken; [Constructible] diff --git a/Projects/UOContent/Items/Weapons/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs index 3bc381b5a..85489edf3 100644 --- a/Projects/UOContent/Items/Weapons/BaseWeapon.cs +++ b/Projects/UOContent/Items/Weapons/BaseWeapon.cs @@ -1026,6 +1026,11 @@ public abstract partial class BaseWeapon public override bool CanEquip(Mobile from) { + if (!from.Player || from.AccessLevel >= AccessLevel.GameMaster) + { + return from.CanBeginAction() && base.CanEquip(from); + } + if (!Ethic.CheckEquip(from, this)) { return false; @@ -3657,7 +3662,7 @@ public abstract partial class BaseWeapon attacker.DoHarmful(defender); - MagerySpell sp = new DispelSpell(attacker); + var sp = new DispelSpell(attacker); if (sp.CheckResisted(defender)) { diff --git a/Projects/UOContent/Migrations/Server.Items.FlameSpurtTrap.v0.json b/Projects/UOContent/Migrations/Server.Items.FlameSpurtTrap.v0.json index 1e92a8ec5..70c2b17fd 100644 --- a/Projects/UOContent/Migrations/Server.Items.FlameSpurtTrap.v0.json +++ b/Projects/UOContent/Migrations/Server.Items.FlameSpurtTrap.v0.json @@ -4,7 +4,7 @@ "properties": [ { "name": "Spurt", - "type": "Server.Item", + "type": "Server.Items.Static", "rule": "SerializableInterfaceMigrationRule" } ] diff --git a/Projects/UOContent/Migrations/Server.Mobiles.LordOaks.v0.json b/Projects/UOContent/Migrations/Server.Mobiles.LordOaks.v0.json index 435aa4121..d210e74a1 100644 --- a/Projects/UOContent/Migrations/Server.Mobiles.LordOaks.v0.json +++ b/Projects/UOContent/Migrations/Server.Mobiles.LordOaks.v0.json @@ -4,7 +4,7 @@ "properties": [ { "name": "Queen", - "type": "Server.Mobiles.BaseCreature", + "type": "Server.Mobiles.Silvani", "rule": "SerializableInterfaceMigrationRule" }, { diff --git a/Projects/UOContent/Misc/AccountPrompt.cs b/Projects/UOContent/Misc/AccountPrompt.cs index fc283892b..032c55526 100644 --- a/Projects/UOContent/Misc/AccountPrompt.cs +++ b/Projects/UOContent/Misc/AccountPrompt.cs @@ -1,4 +1,3 @@ -using System; using Server.Accounting; using Server.Logging; @@ -12,18 +11,16 @@ public static class AccountPrompt { if (Accounts.Count == 0) { - Console.WriteLine("This server has no accounts."); - Console.Write("Do you want to create the owner account now? (y/n): "); + logger.Warning("This server has no accounts."); + logger.Information("Do you want to create the owner account now? (y/n):"); var answer = ConsoleInputHandler.ReadLine(); if (answer.InsensitiveEquals("y")) { - Console.WriteLine(); - - Console.Write("Username: "); + logger.Information("Input Username:"); var username = ConsoleInputHandler.ReadLine(); - Console.Write("Password: "); + logger.Information("Input Password:"); var password = ConsoleInputHandler.ReadLine(); var a = new Account(username, password) diff --git a/Projects/UOContent/Misc/AdminFirewall.cs b/Projects/UOContent/Misc/AdminFirewall.cs index 684546fba..a4275d90c 100644 --- a/Projects/UOContent/Misc/AdminFirewall.cs +++ b/Projects/UOContent/Misc/AdminFirewall.cs @@ -127,10 +127,13 @@ public static class AdminFirewall public static void Save() { - using var op = new StreamWriter(firewallConfigPath); - foreach (var entry in Firewall.FirewallSet) + Firewall.ReadFirewallSet(firewallSet => { - op.WriteLine(entry); - } + using var op = new StreamWriter(firewallConfigPath); + foreach (var entry in firewallSet) + { + op.WriteLine(entry); + } + }); } } diff --git a/Projects/UOContent/Misc/ClientVerification.cs b/Projects/UOContent/Misc/ClientVerification.cs index 27aac5f1f..893dc80b3 100644 --- a/Projects/UOContent/Misc/ClientVerification.cs +++ b/Projects/UOContent/Misc/ClientVerification.cs @@ -27,14 +27,12 @@ namespace Server.Misc public static bool AllowKR => (AllowedClientTypes & ClientType.KR) != 0; public static bool AllowSA => (AllowedClientTypes & ClientType.SA) != 0; - public static ClientVersion MinRequired { get; private set; } - public static ClientVersion MaxRequired { get; private set; } public static TimeSpan KickDelay { get; private set; } public static void Configure() { - MinRequired = ServerConfiguration.GetSetting("clientVerification.minRequired", (ClientVersion)null); - MaxRequired = ServerConfiguration.GetSetting("clientVerification.maxRequired", (ClientVersion)null); + UOClient.MinRequired = ServerConfiguration.GetSetting("clientVerification.minRequired", (ClientVersion)null); + UOClient.MaxRequired = ServerConfiguration.GetSetting("clientVerification.maxRequired", (ClientVersion)null); _enable = ServerConfiguration.GetOrUpdateSetting("clientVerification.enable", true); _invalidClientResponse = @@ -51,12 +49,12 @@ namespace Server.Misc public static void Initialize() { - if (MinRequired == null && MaxRequired == null) + if (UOClient.MinRequired == null && UOClient.MaxRequired == null) { - MinRequired = UOClient.ServerClientVersion; + UOClient.MinRequired = UOClient.ServerClientVersion; } - if (MinRequired != null || MaxRequired != null) + if (UOClient.MinRequired != null || UOClient.MaxRequired != null) { logger.Information( "Restricting client version to {ClientVersion}. Action to be taken: {Action}", @@ -70,17 +68,17 @@ namespace Server.Misc { if (_versionExpression == null) { - if (MinRequired != null && MaxRequired != null) + if (UOClient.MinRequired != null && UOClient.MaxRequired != null) { - _versionExpression = $"{MinRequired}-{MaxRequired}"; + _versionExpression = $"{UOClient.MinRequired}-{UOClient.MaxRequired}"; } - else if (MinRequired != null) + else if (UOClient.MinRequired != null) { - _versionExpression = $"{MinRequired} or newer"; + _versionExpression = $"{UOClient.MinRequired} or newer"; } else { - _versionExpression = $"{MaxRequired} or older"; + _versionExpression = $"{UOClient.MaxRequired} or older"; } } @@ -133,14 +131,14 @@ namespace Server.Misc bool shouldKick = false; bool isKRClient = version.Type == ClientType.KR; - if (!isKRClient && MinRequired != null && version < MinRequired) + if (!isKRClient && UOClient.MinRequired != null && version < UOClient.MinRequired) { - sb.Append($"This server doesn't support clients older than {MinRequired}."); + sb.Append($"This server doesn't support clients older than {UOClient.MinRequired}."); shouldKick = strictRequirement; } - else if (!isKRClient && MaxRequired != null && version > MaxRequired) + else if (!isKRClient && UOClient.MaxRequired != null && version > UOClient.MaxRequired) { - sb.Append($"This server doesn't support clients newer than {MaxRequired}."); + sb.Append($"This server doesn't support clients newer than {UOClient.MaxRequired}."); shouldKick = strictRequirement; } else diff --git a/Projects/UOContent/Misc/Guild.cs b/Projects/UOContent/Misc/Guild.cs index 3c9707160..66e4d5e2d 100644 --- a/Projects/UOContent/Misc/Guild.cs +++ b/Projects/UOContent/Misc/Guild.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using Server.Commands.Generic; using Server.Gumps; using Server.Items; @@ -609,7 +610,7 @@ namespace Server.Guilds { get { - if (Disbanded || m_Leader.Guild != this) + if (Disbanded) { CalculateGuildmaster(); } @@ -777,7 +778,7 @@ namespace Server.Guilds public void InvalidateMemberProperties(bool onlyOPL = false) { - for (var i = 0; i < Members?.Count; i++) + for (var i = 0; i < Members.Count; i++) { var m = Members[i]; m.InvalidateProperties(); @@ -791,7 +792,7 @@ namespace Server.Guilds public void InvalidateMemberNotoriety() { - for (var i = 0; i < Members?.Count; i++) + for (var i = 0; i < Members.Count; i++) { Members[i].Delta(MobileDelta.Noto); } @@ -1220,14 +1221,14 @@ namespace Server.Guilds { var count = reader.ReadInt(); - PendingWars = new List(); + PendingWars = new List(count); for (var i = 0; i < count; i++) { PendingWars.Add(new WarDeclaration(reader)); } count = reader.ReadInt(); - AcceptedWars = new List(); + AcceptedWars = new List(count); for (var i = 0; i < count; i++) { AcceptedWars.Add(new WarDeclaration(reader)); @@ -1275,11 +1276,6 @@ namespace Server.Guilds { m_Leader = reader.ReadEntity(); - if (m_Leader is PlayerMobile mobile) - { - mobile.GuildRank = RankDefinition.Leader; - } - m_Name = reader.ReadString(); m_Abbreviation = reader.ReadString(); @@ -1334,66 +1330,70 @@ namespace Server.Guilds public void AddMember(Mobile m) { - if (!Members.Contains(m)) + if (Members.Contains(m)) { - if (m.Guild != null && m.Guild != this) - { - ((Guild)m.Guild).RemoveMember(m); - } - - Members.Add(m); - m.Guild = this; - - m.GuildFealty = !NewGuildSystem ? m_Leader : null; - - if (m is PlayerMobile mobile) - { - mobile.GuildRank = RankDefinition.Lowest; - } - - ((Guild)m.Guild).InvalidateWarNotoriety(); + return; } + + var oldGuild = m.Guild as Guild; + + if (oldGuild != this) + { + oldGuild?.RemoveMember(m); + } + + Members.Add(m); + m.Guild = this; + + m.GuildFealty = !NewGuildSystem ? m_Leader : null; + + if (m is PlayerMobile pm) + { + pm.GuildRank = RankDefinition.Lowest; + } + + oldGuild?.InvalidateWarNotoriety(); + InvalidateWarNotoriety(); } public void RemoveMember(Mobile m, int message = 1018028) // You have been dismissed from your guild. { - if (Members.Contains(m)) + if (!Members.Remove(m)) { - Members.Remove(m); + return; + } - var guild = m.Guild as Guild; + var oldGuild = m.Guild as Guild; + m.Guild = null; - m.Guild = null; + if (m is PlayerMobile pm) + { + pm.GuildRank = RankDefinition.Lowest; + } - if (m is PlayerMobile mobile) - { - mobile.GuildRank = RankDefinition.Lowest; - } + if (message > 0) + { + m.SendLocalizedMessage(message); + } - if (message > 0) - { - m.SendLocalizedMessage(message); - } + if (m == m_Leader) + { + CalculateGuildmaster(); - if (m == m_Leader) - { - CalculateGuildmaster(); - - if (m_Leader == null) - { - Disband(); - } - } - - if (Members.Count == 0) + if (m_Leader == null) { Disband(); } - - guild?.InvalidateWarNotoriety(); - - m.Delta(MobileDelta.Noto); } + + if (Members.Count == 0) + { + Disband(); + } + + oldGuild?.InvalidateWarNotoriety(); + + m.Delta(MobileDelta.Noto); } public void AddAlly(Guild g) @@ -1408,10 +1408,8 @@ namespace Server.Guilds public void RemoveAlly(Guild g) { - if (Allies.Contains(g)) + if (Allies.Remove(g)) { - Allies.Remove(g); - g.RemoveAlly(this); } } @@ -1428,10 +1426,8 @@ namespace Server.Guilds public void RemoveEnemy(Guild g) { - if (Enemies.Contains(g)) + if (Enemies.Remove(g)) { - Enemies.Remove(g); - g.RemoveEnemy(this); } } @@ -1507,9 +1503,12 @@ namespace Server.Guilds { var votes = new Dictionary(); + // When the leader resigns, this will be false + var disbanded = Disbanded; + var hasLeader = !disbanded && m_Leader?.Guild == this; var votingMembers = 0; - for (var i = 0; i < Members?.Count; ++i) + for (var i = 0; i < Members.Count; ++i) { var memb = Members[i]; @@ -1522,14 +1521,7 @@ namespace Server.Guilds if (!CanBeVotedFor(m)) { - if (!Disbanded && m_Leader.Guild == this) - { - m = m_Leader; - } - else - { - m = memb; - } + m = hasLeader ? m_Leader : memb; } if (m == null) @@ -1537,18 +1529,16 @@ namespace Server.Guilds continue; } - votes[m] = 1 + (votes.TryGetValue(m, out var v) ? v : 0); + ref var voteCount = ref CollectionsMarshal.GetValueRefOrAddDefault(votes, m, out _); + voteCount++; votingMembers++; } Mobile winner = null; var highVotes = 0; - foreach (var kvp in votes) + foreach (var (m, val) in votes) { - var m = kvp.Key; - var val = kvp.Value; - if (winner == null || val > highVotes) { winner = m; @@ -1556,13 +1546,34 @@ namespace Server.Guilds } } - if (NewGuildSystem && highVotes * 100 / Math.Max(votingMembers, 1) < MajorityPercentage && !Disbanded && - winner != m_Leader && m_Leader.Guild == this) + if (hasLeader && (winner == null || + NewGuildSystem && highVotes * 100 / Math.Max(votingMembers, 1) < MajorityPercentage)) { winner = m_Leader; } - if (m_Leader != winner && winner != null) + if (winner == null) + { + if (votes.Count > 0) + { + var randomNumber = Utility.Random(votes.Count); + var index = 0; + foreach (var m in votes.Keys) + { + if (index++ == randomNumber) + { + winner = m; + break; + } + } + } + else + { + winner = Members.RandomElement(); + } + } + + if (winner != null && m_Leader != winner) { Leader = winner; GuildMessage(1018015, true, winner.RawName); // Guild Message: Guildmaster changed to: diff --git a/Projects/UOContent/Misc/InhumanSpeech.cs b/Projects/UOContent/Misc/InhumanSpeech.cs index 41f0e51b7..4aafad2f8 100644 --- a/Projects/UOContent/Misc/InhumanSpeech.cs +++ b/Projects/UOContent/Misc/InhumanSpeech.cs @@ -360,10 +360,10 @@ namespace Server.Misc return sentence.ToString(); } - public void SayRandomTranslate(Mobile mob, params string[] sentancesInEnglish) + public void SayRandomTranslate(Mobile mob, params ReadOnlySpan sentencesInEnglish) { SaySentance(mob, Utility.RandomMinMax(2, 3)); - mob.Say(sentancesInEnglish.RandomElement()); + mob.Say(sentencesInEnglish.RandomElement()); } private string GetRandomResponseWord(List keywordsFound) diff --git a/Projects/UOContent/Misc/Loot.cs b/Projects/UOContent/Misc/Loot.cs index 82a73054b..b641400a2 100644 --- a/Projects/UOContent/Misc/Loot.cs +++ b/Projects/UOContent/Misc/Loot.cs @@ -766,15 +766,15 @@ namespace Server } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Item Construct(params Type[] types) => Construct(types.RandomElement()); + public static Item Construct(params ReadOnlySpan types) => Construct(types.RandomElement()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T Construct(params Type[] types) where T : Item => Construct(types.RandomElement()); + public static T Construct(params ReadOnlySpan types) where T : Item => Construct(types.RandomElement()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Item Construct(Type[] types, int index) => Construct(types, index); + public static Item Construct(ReadOnlySpan types, int index) => Construct(types, index); - public static T Construct(Type[] types, int index) where T : Item + public static T Construct(ReadOnlySpan types, int index) where T : Item { if (index >= 0 && index < types.Length) { diff --git a/Projects/UOContent/Misc/NameVerification.cs b/Projects/UOContent/Misc/NameVerification.cs index e64c41bf9..ae934b261 100644 --- a/Projects/UOContent/Misc/NameVerification.cs +++ b/Projects/UOContent/Misc/NameVerification.cs @@ -1,247 +1,290 @@ using System; +using System.Buffers; +using System.Runtime.CompilerServices; -namespace Server.Misc +namespace Server.Misc; + +public static class NameVerification { - public static class NameVerification - { - public static readonly char[] SpaceDashPeriodQuote = - { - ' ', '-', '.', '\'' - }; + public static readonly SearchValues AlphaNumeric = SearchValues.Create( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z' + ); - public static readonly char[] Empty = Array.Empty(); + public static readonly SearchValues Alphabetic = SearchValues.Create( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z' + ); - public static string[] StartDisallowed { get; } = - { + public static readonly SearchValues Numeric = SearchValues.Create( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + ); + + public static readonly SearchValues SpaceDashPeriodQuote = SearchValues.Create(' ', '-', '.', '\''); + + public static readonly SearchValues StartDisallowed = SearchValues.Create( + [ "seer", "counselor", "gm", "admin", "lady", "lord" - }; + ], + StringComparison.OrdinalIgnoreCase + ); - public static string[] Disallowed { get; } = - { - "jigaboo", - "chigaboo", - "wop", - "kyke", - "kike", - "tit", - "spic", - "prick", - "piss", - "lezbo", - "lesbo", - "felatio", - "dyke", - "dildo", - "chinc", - "chink", - "cunnilingus", - "cum", - "cocksucker", - "cock", - "clitoris", - "clit", - "ass", - "hitler", - "penis", - "nigga", - "nigger", - "klit", - "kunt", - "jiz", - "jism", - "jerkoff", - "jackoff", - "goddamn", - "fag", - "blowjob", - "bitch", - "asshole", - "dick", - "pussy", - "snatch", - "cunt", - "twat", - "shit", - "fuck", - "tailor", - "smith", - "scholar", - "rogue", - "novice", - "neophyte", - "merchant", - "medium", - "master", - "mage", - "lb", - "journeyman", - "grandmaster", - "fisherman", - "expert", - "chef", - "carpenter", - "british", - "blackthorne", - "blackthorn", - "beggar", - "archer", - "apprentice", - "adept", - "gamemaster", - "frozen", - "squelched", - "invulnerable", - "osi", - "origin" - }; + public static readonly string[] Disallowed = + [ + ..ProfanityProtection.Disallowed, + "tailor", + "smith", + "scholar", + "rogue", + "novice", + "neophyte", + "merchant", + "medium", + "master", + "mage", + "lb", + "journeyman", + "grandmaster", + "fisherman", + "expert", + "chef", + "carpenter", + "british", + "blackthorne", + "blackthorn", + "beggar", + "archer", + "apprentice", + "adept", + "gamemaster", + "frozen", + "squelched", + "invulnerable", + "osi", + "origin" + ]; - public static void Configure() + public static readonly SearchValues DisallowedSearchValues = SearchValues.Create( + Disallowed, + StringComparison.OrdinalIgnoreCase + ); + + public static void Configure() + { + CommandSystem.Register("ValidateName", AccessLevel.Administrator, ValidateName_OnCommand); + } + + [Usage("ValidateName"), Description("Checks the result of NameValidation on the specified name.")] + public static void ValidateName_OnCommand(CommandEventArgs e) + { + if (Validate(e.ArgString, 2, 16, true, false, true, 1, SpaceDashPeriodQuote)) { - CommandSystem.Register("ValidateName", AccessLevel.Administrator, ValidateName_OnCommand); + e.Mobile.SendMessage(0x59, "That name is considered valid."); + } + else + { + e.Mobile.SendMessage(0x22, "That name is considered invalid."); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ValidatePlayerName(ReadOnlySpan name) => + Validate(name, 2, 16, true, false, true, 1, SpaceDashPeriodQuote); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ValidatePetName(ReadOnlySpan name) => Validate(name, 1, 16, true, false, exceptions: null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ValidateVendorName(ReadOnlySpan name) => Validate(name, 1, 20, true, true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Validate( + ReadOnlySpan name, int minLength, int maxLength, bool allowLetters, bool allowDigits, + bool noExceptionsAtStart = true, int maxExceptions = 0, SearchValues exceptions = null + ) => Validate( + name, + minLength, + maxLength, + allowLetters, + allowDigits, + noExceptionsAtStart, + maxExceptions, + exceptions, + Disallowed, + DisallowedSearchValues, + StartDisallowed + ); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Validate( + ReadOnlySpan name, int minLength, int maxLength, bool allowLetters, bool allowDigits, + bool noExceptionsAtStart, int maxExceptions, SearchValues exceptions, ReadOnlySpan disallowed, + SearchValues disallowedSV + ) => Validate( + name, + minLength, + maxLength, + allowLetters, + allowDigits, + noExceptionsAtStart, + maxExceptions, + exceptions, + disallowed, + disallowedSV, + null + ); + + public static bool Validate( + ReadOnlySpan name, int minLength, int maxLength, bool allowLetters, bool allowDigits, + bool noExceptionsAtStart, int maxExceptions, SearchValues exceptions, ReadOnlySpan disallowed, + SearchValues disallowedSV, SearchValues startDisallowedSV + ) + { + if (name.Length == 0 || name.Length < minLength || name.Length > maxLength) + { + return false; } - [Usage("ValidateName"), Description("Checks the result of NameValidation on the specified name.")] - public static void ValidateName_OnCommand(CommandEventArgs e) + if (exceptions == null) { - if (Validate(e.ArgString, 2, 16, true, false, true, 1, SpaceDashPeriodQuote)) + // We don't have exceptions, so we might be limited to letters or numbers + var allowed = allowLetters switch { - e.Mobile.SendMessage(0x59, "That name is considered valid."); - } - else + // If we don't allow exceptions, then non-alphanumeric is not allowed + true when allowDigits && maxExceptions == 0 => AlphaNumeric, + true when !allowDigits => Alphabetic, + false when allowDigits => Numeric, + // Everything has been allowed! Use `Utility.FixHtml()` to stop weird behavior + _ => null + }; + + if (allowed != null && name.ContainsAnyExcept(allowed)) { - e.Mobile.SendMessage(0x22, "That name is considered invalid."); + return false; } } - - public static bool Validate( - string name, int minLength, int maxLength, bool allowLetters, bool allowDigits, - bool noExceptionsAtStart, int maxExceptions, char[] exceptions - ) => - Validate( - name, - minLength, - maxLength, - allowLetters, - allowDigits, - noExceptionsAtStart, - maxExceptions, - exceptions, - Disallowed, - StartDisallowed - ); - - public static bool Validate( - string name, int minLength, int maxLength, bool allowLetters, bool allowDigits, - bool noExceptionsAtStart, int maxExceptions, char[] exceptions, string[] disallowed, string[] startDisallowed - ) + else { - if (name == null || name.Length < minLength || name.Length > maxLength) + // We have exceptions, and at least one of the letters/digits flag is false: + var notAllowed = allowLetters switch + { + true when !allowDigits => Numeric, + false when allowDigits => Alphabetic, + _ => null + }; + + if (notAllowed != null && name.ContainsAny(notAllowed)) { return false; } - var exceptCount = 0; - - name = name.ToLower(); - - if (!allowLetters || !allowDigits || - exceptions.Length > 0 && (noExceptionsAtStart || maxExceptions < int.MaxValue)) + if (ContainsExceptions(name, exceptions, noExceptionsAtStart, maxExceptions)) { - for (var i = 0; i < name.Length; ++i) - { - var c = name[i]; - - if (c >= 'a' && c <= 'z') - { - if (!allowLetters) - { - return false; - } - - exceptCount = 0; - } - else if (c >= '0' && c <= '9') - { - if (!allowDigits) - { - return false; - } - - exceptCount = 0; - } - else - { - var except = false; - - for (var j = 0; !except && j < exceptions.Length; ++j) - { - if (c == exceptions[j]) - { - except = true; - } - } - - if (!except || i == 0 && noExceptionsAtStart) - { - return false; - } - - if (exceptCount++ == maxExceptions) - { - return false; - } - } - } + return false; } - - for (var i = 0; i < disallowed.Length; ++i) - { - var indexOf = name.IndexOfOrdinal(disallowed[i]); - - if (indexOf == -1) - { - continue; - } - - var badPrefix = indexOf == 0; - - for (var j = 0; !badPrefix && j < exceptions.Length; ++j) - { - badPrefix = name[indexOf - 1] == exceptions[j]; - } - - if (!badPrefix) - { - continue; - } - - var badSuffix = indexOf + disallowed[i].Length >= name.Length; - - for (var j = 0; !badSuffix && j < exceptions.Length; ++j) - { - badSuffix = name[indexOf + disallowed[i].Length] == exceptions[j]; - } - - if (badSuffix) - { - return false; - } - } - - for (var i = 0; i < startDisallowed.Length; ++i) - { - if (name.StartsWithOrdinal(startDisallowed[i])) - { - return false; - } - } - - return true; } + + if (disallowedSV != null && disallowed.Length > 0 && ContainsDisallowedWord(name, disallowed, disallowedSV)) + { + return false; + } + + return startDisallowedSV == null || name.IndexOfAny(startDisallowedSV) != 0; + } + + public static bool ContainsExceptions( + ReadOnlySpan name, SearchValues exceptions, bool noExceptionsAtStart, int maxExceptions + ) + { + if (!noExceptionsAtStart && maxExceptions is <= -1 or >= int.MaxValue) + { + return false; + } + + var index = name.IndexOfAny(exceptions); + + while (index != -1) + { + if (noExceptionsAtStart) + { + if (index == 0) + { + return true; + } + + noExceptionsAtStart = false; + } + + if (maxExceptions-- <= 0) + { + return true; + } + + if (index + 1 < name.Length) + { + name = name[(index + 1)..]; + index = name.IndexOfAny(exceptions); + } + else + { + index = -1; + } + } + + return false; + } + + public static bool ContainsDisallowedWord(ReadOnlySpan name, ReadOnlySpan disallowed, SearchValues disallowedSV) + { + var index = name.IndexOfAny(disallowedSV); + + while (index != -1) + { + var isStartBoundary = index == 0 || !char.IsLetterOrDigit(name[index - 1]); + + if (isStartBoundary) + { + for (var i = 0; i < disallowed.Length; i++) + { + var word = disallowed[i].AsSpan(); + if (index + word.Length > name.Length || !name.Slice(index, word.Length).InsensitiveEquals(word)) + { + continue; + } + + // End boundary + if (index + word.Length == name.Length || !char.IsLetterOrDigit(name[index + word.Length])) + { + return true; + } + } + } + + if (index + 1 < name.Length) + { + name = name[(index + 1)..]; + index = name.IndexOfAny(disallowedSV); + } + else + { + index = -1; + } + } + + return false; } } diff --git a/Projects/UOContent/Misc/Notoriety.cs b/Projects/UOContent/Misc/Notoriety.cs index 3d8a5b6c6..ff7924bb5 100644 --- a/Projects/UOContent/Misc/Notoriety.cs +++ b/Projects/UOContent/Misc/Notoriety.cs @@ -387,6 +387,12 @@ namespace Server.Misc return Notoriety.Invulnerable; } + // Moved above AccessLevel check so staff summons are red + if (bcTarg?.AlwaysMurderer == true) + { + return Notoriety.Murderer; + } + var pmFrom = source as PlayerMobile; var pmTarg = target as PlayerMobile; @@ -434,7 +440,7 @@ namespace Server.Misc if (target.Kills >= 5 || target.Body.IsMonster && IsSummoned(bcTarg) && target is not BaseFamiliar && target is not ArcaneFey && - target is not Golem || bcTarg?.AlwaysMurderer == true || bcTarg?.IsAnimatedDead == true) + target is not Golem || bcTarg?.IsAnimatedDead == true) { return Notoriety.Murderer; } diff --git a/Projects/UOContent/Misc/ProfanityProtection.cs b/Projects/UOContent/Misc/ProfanityProtection.cs index cf917938e..9aa0280d4 100644 --- a/Projects/UOContent/Misc/ProfanityProtection.cs +++ b/Projects/UOContent/Misc/ProfanityProtection.cs @@ -1,140 +1,147 @@ using System; +using System.Buffers; -namespace Server.Misc +namespace Server.Misc; + +public enum ProfanityAction { - public enum ProfanityAction + None, // no action taken + Disallow, // speech is not displayed + Criminal, // makes the player criminal, not killable by guards + CriminalAction, // makes the player criminal, can be killed by guards + Disconnect, // player is kicked + Other // some other implementation +} + +public static class ProfanityProtection +{ + private static bool Enabled; + private static ProfanityAction Action; + + public static void Configure() { - None, // no action taken - Disallow, // speech is not displayed - Criminal, // makes the player criminal, not killable by guards - CriminalAction, // makes the player criminal, can be killed by guards - Disconnect, // player is kicked - Other // some other implementation + Enabled = ServerConfiguration.GetSetting("profanityProtection.enabled", false); + Action = ServerConfiguration.GetSetting("profanityProtection.action", ProfanityAction.Disallow); + + if (Enabled) + { + EventSink.Speech += EventSink_Speech; + } } - public static class ProfanityProtection + // Used by the guild system + public static readonly SearchValues Exceptions = SearchValues.Create( + ' ', '-', '.', '\'', '"', ',', '_', '+', '=', '~', '`', '!', '^', '*', '\\', '/', ';', ':', '<', '>', '[', ']', + '{', '}', '?', '|', '(', ')', '%', '$', '&', '#', '@' + ); + + public static readonly string[] Disallowed = + [ + "jigaboo", + "chigaboo", + "wop", + "kyke", + "kike", + "tit", + "spic", + "prick", + "piss", + "lezbo", + "lesbo", + "felatio", + "dyke", + "dildo", + "chinc", + "chink", + "cunnilingus", + "cum", + "cocksucker", + "cock", + "clitoris", + "clit", + "ass", + "hitler", + "penis", + "nigga", + "nigger", + "klit", + "kunt", + "jiz", + "jism", + "jerkoff", + "jackoff", + "goddamn", + "fag", + "blowjob", + "bitch", + "asshole", + "dick", + "pussy", + "snatch", + "cunt", + "twat", + "shit", + "fuck", + ]; + + public static readonly SearchValues DisallowedSearchValues = SearchValues.Create( + Disallowed, + StringComparison.OrdinalIgnoreCase + ); + + private static bool OnProfanityDetected(Mobile from, string speech) { - // TODO: Move this to configuration - private static readonly bool Enabled = false; - - private static readonly ProfanityAction - Action = ProfanityAction.Disallow; // change here what to do when profanity is detected - - public static char[] Exceptions { get; } = + switch (Action) { - ' ', '-', '.', '\'', '"', ',', '_', '+', '=', '~', '`', '!', '^', '*', '\\', '/', ';', ':', '<', '>', '[', ']', - '{', '}', '?', '|', '(', ')', '%', '$', '&', '#', '@' - }; + case ProfanityAction.None: return true; + case ProfanityAction.Disallow: return false; + case ProfanityAction.Criminal: + from.Criminal = true; + return true; + case ProfanityAction.CriminalAction: + from.CriminalAction(false); + return true; + case ProfanityAction.Disconnect: + { + from.NetState?.Disconnect("Using profanity."); - public static string[] StartDisallowed { get; } = Array.Empty(); + return false; + } + default: + case ProfanityAction.Other: // TODO: Provide custom implementation if this is chosen + { + return true; + } + } + } - public static string[] Disallowed { get; } = + public static bool ContainsProfanity(ReadOnlySpan speech) => + speech.Length > 0 && + !NameVerification.Validate( + speech, + 1, + int.MaxValue, + true, + true, + true, + int.MaxValue, // allow all non-alphanumeric characters + null, + Disallowed, + DisallowedSearchValues + ); + + private static void EventSink_Speech(SpeechEventArgs e) + { + var from = e.Mobile; + + if (from.AccessLevel > AccessLevel.Player) { - "jigaboo", - "chigaboo", - "wop", - "kyke", - "kike", - "tit", - "spic", - "prick", - "piss", - "lezbo", - "lesbo", - "felatio", - "dyke", - "dildo", - "chinc", - "chink", - "cunnilingus", - "cum", - "cocksucker", - "cock", - "clitoris", - "clit", - "ass", - "hitler", - "penis", - "nigga", - "nigger", - "klit", - "kunt", - "jiz", - "jism", - "jerkoff", - "jackoff", - "goddamn", - "fag", - "blowjob", - "bitch", - "asshole", - "dick", - "pussy", - "snatch", - "cunt", - "twat", - "shit", - "fuck" - }; - - public static void Initialize() - { - if (Enabled) - { - EventSink.Speech += EventSink_Speech; - } + return; } - private static bool OnProfanityDetected(Mobile from, string speech) + if (ContainsProfanity(e.Speech)) { - switch (Action) - { - case ProfanityAction.None: return true; - case ProfanityAction.Disallow: return false; - case ProfanityAction.Criminal: - from.Criminal = true; - return true; - case ProfanityAction.CriminalAction: - from.CriminalAction(false); - return true; - case ProfanityAction.Disconnect: - { - from.NetState?.Disconnect("Using profanity."); - - return false; - } - default: - case ProfanityAction.Other: // TODO: Provide custom implementation if this is chosen - { - return true; - } - } - } - - private static void EventSink_Speech(SpeechEventArgs e) - { - var from = e.Mobile; - - if (from.AccessLevel > AccessLevel.Player) - { - return; - } - - if (!NameVerification.Validate( - e.Speech, - 0, - int.MaxValue, - true, - true, - false, - int.MaxValue, - Exceptions, - Disallowed, - StartDisallowed - )) - { - e.Blocked = !OnProfanityDetected(from, e.Speech); - } + e.Blocked = !OnProfanityDetected(from, e.Speech); } } } diff --git a/Projects/UOContent/Misc/RenameRequests.cs b/Projects/UOContent/Misc/RenameRequests.cs index ffaece37e..8abf648d7 100644 --- a/Projects/UOContent/Misc/RenameRequests.cs +++ b/Projects/UOContent/Misc/RenameRequests.cs @@ -1,54 +1,31 @@ using System; -namespace Server.Misc +namespace Server.Misc; + +public static class RenameRequests { - public static class RenameRequests + public static void RenameRequest(Mobile from, Mobile targ, string name) { - public static void RenameRequest(Mobile from, Mobile targ, string name) + if (!from.CanSee(targ) || !from.InRange(targ, 12) || !targ.CanBeRenamedBy(from)) { - if (from.CanSee(targ) && from.InRange(targ, 12) && targ.CanBeRenamedBy(from)) - { - name = name.Trim(); + return; + } - if (NameVerification.Validate( - name, - 1, - 16, - true, - false, - true, - 0, - NameVerification.Empty, - NameVerification.StartDisallowed, - Core.ML ? NameVerification.Disallowed : Array.Empty() - )) - { - if (Core.ML) - { - var disallowed = ProfanityProtection.Disallowed; + var span = name.AsSpan().Trim(); - for (var i = 0; i < disallowed.Length; i++) - { - if (name.IndexOfOrdinal(disallowed[i]) != -1) - { - from.SendLocalizedMessage(1072622); // That name isn't very polite. - return; - } - } - - from.SendLocalizedMessage( - 1072623, - $"{targ.Name}\t{name}" - ); // Pet ~1_OLDPETNAME~ renamed to ~2_NEWPETNAME~. - } - - targ.Name = name; - } - else - { - from.SendMessage("That name is unacceptable."); - } - } + if (NameVerification.ValidatePetName(span)) + { + // Pet ~1_OLDPETNAME~ renamed to ~2_NEWPETNAME~. + from.SendLocalizedMessage(1072623, $"{targ.Name}\t{span}"); + targ.Name = span.ToString(); + } + else if (span.IndexOfAny(ProfanityProtection.DisallowedSearchValues) != -1) + { + from.SendLocalizedMessage(1072622); // That name isn't very polite. + } + else + { + from.SendMessage("That name is unacceptable."); } } } diff --git a/Projects/UOContent/Misc/Weather.cs b/Projects/UOContent/Misc/Weather.cs index a29b7e985..178e233b8 100644 --- a/Projects/UOContent/Misc/Weather.cs +++ b/Projects/UOContent/Misc/Weather.cs @@ -15,14 +15,14 @@ namespace Server.Misc private int m_Stage; public Weather( - Map facet, Rectangle2D[] area, int temperature, int chanceOfPercipitation, int chanceOfExtremeTemperature, + Map facet, Rectangle2D[] area, int temperature, int chanceOfPrecipitation, int chanceOfExtremeTemperature, TimeSpan interval ) { Facet = facet; Area = area; Temperature = temperature; - ChanceOfPercipitation = chanceOfPercipitation; + ChanceOfPrecipitation = chanceOfPrecipitation; ChanceOfExtremeTemperature = chanceOfExtremeTemperature; var list = GetWeatherList(facet); @@ -42,7 +42,7 @@ namespace Server.Misc public int Temperature { get; set; } - public int ChanceOfPercipitation { get; set; } + public int ChanceOfPrecipitation { get; set; } public int ChanceOfExtremeTemperature { get; set; } @@ -298,7 +298,7 @@ namespace Server.Misc { if (m_Stage == 0) { - m_Active = ChanceOfPercipitation > Utility.Random(100); + m_Active = ChanceOfPrecipitation > Utility.Random(100); m_ExtremeTemperature = ChanceOfExtremeTemperature > Utility.Random(100); if (MoveSpeed > 0) diff --git a/Projects/UOContent/Mobiles/AI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI.cs index 5ce7ba5a7..d7cce9923 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI.cs @@ -1899,8 +1899,7 @@ public abstract class BaseAI } else { - Container c = fromState.AddTrade(toState); - c.DropItem(new TransferItem(m_Mobile)); + fromState.AddTrade(toState).DropItem(new TransferItem(m_Mobile)); } } } @@ -2672,39 +2671,27 @@ public abstract class BaseAI var pm = m as PlayerMobile; // Monster don't attack it's own summon or the summon of another monster - if (Core.AOS && bc != null && bc.Summoned && (bc.SummonMaster == m_Mobile || (!bc.SummonMaster.Player && IsHostile(bc.SummonMaster)))) + if (Core.AOS && bc != null && bc.Summoned && (bc.SummonMaster == m_Mobile || !bc.SummonMaster.Player && IsHostile(bc.SummonMaster))) { continue; } if (m_Mobile.Summoned && m_Mobile.SummonMaster != null) { - // If this is a summon, it can't target its controller. - if (m == m_Mobile.SummonMaster) - { - continue; - } - - // It also must abide by harmful spell rules. - if (!SpellHelper.ValidIndirectTarget(m_Mobile.SummonMaster, m)) - { - continue; - } - // Animated creatures cannot attack players directly. if (pm != null && m_Mobile.IsAnimatedDead) { continue; } - // Animated creatures cannot attack other animated creatures - if (m_Mobile.IsAnimatedDead && bc?.IsAnimatedDead == true) + // Animated creatures cannot attack other animated creatures or pets of other players + if (m_Mobile.IsAnimatedDead && bc != null && (bc.IsAnimatedDead || bc.Controlled)) { continue; } - // Animated creatures cannot attack pets of other players - if (m_Mobile.IsAnimatedDead && bc?.Controlled == true) + // If this is a summon, it can't target its controller or invalid targets. + if (m_Mobile.FollowsAcquireRules && (m == m_Mobile.SummonMaster || !SpellHelper.ValidIndirectTarget(m_Mobile, m))) { continue; } @@ -2771,7 +2758,9 @@ public abstract class BaseAI } var theirVal = m_Mobile.GetFightModeRanking(m, acqType, bPlayerOnly); - if (theirVal > val && m_Mobile.InLOS(m)) + + // Always prefer someone else over the summon master (EV/BS) + if ((theirVal > val || newFocusMob == m_Mobile.SummonMaster) && m_Mobile.InLOS(m)) { newFocusMob = m; val = theirVal; diff --git a/Projects/UOContent/Mobiles/AI/HealerAI.cs b/Projects/UOContent/Mobiles/AI/HealerAI.cs index 9a08703ea..4afa17e3b 100644 --- a/Projects/UOContent/Mobiles/AI/HealerAI.cs +++ b/Projects/UOContent/Mobiles/AI/HealerAI.cs @@ -1,3 +1,4 @@ +using System; using Server.Spells; using Server.Spells.First; using Server.Spells.Fourth; @@ -132,7 +133,7 @@ public class HealerAI : BaseAI } } - private Mobile Find(params NeedDelegate[] funcs) + private Mobile Find(params ReadOnlySpan funcs) { if (m_Mobile.Deleted) { diff --git a/Projects/UOContent/Mobiles/Abilities/MagicalBarrier.cs b/Projects/UOContent/Mobiles/Abilities/MagicalBarrier.cs index 585a7695b..2069448be 100644 --- a/Projects/UOContent/Mobiles/Abilities/MagicalBarrier.cs +++ b/Projects/UOContent/Mobiles/Abilities/MagicalBarrier.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace Server.Mobiles; @@ -7,7 +8,7 @@ public class MagicalBarrier : MonsterAbility { private HashSet _inactiveField; - public bool HasField(Mobile source) => !_inactiveField.Contains(source); + public bool HasField(Mobile source) => _inactiveField?.Contains(source) != true; public override MonsterAbilityType AbilityType => MonsterAbilityType.MagicalBarrier; @@ -17,17 +18,20 @@ public class MagicalBarrier : MonsterAbility public override TimeSpan MinTriggerCooldown => TimeSpan.FromSeconds(10.0); public override TimeSpan MaxTriggerCooldown => TimeSpan.FromSeconds(10.0); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanUseField(BaseCreature source) => source.Hits >= source.HitsMax * 9 / 10; + // Regeneration is subject to the cooldown, the rest are not. public override bool CanTrigger(BaseCreature source, MonsterAbilityTrigger trigger) => trigger != MonsterAbilityTrigger.Think || base.CanTrigger(source, trigger); public override void Trigger(MonsterAbilityTrigger trigger, BaseCreature source, Mobile target) { - if (trigger == MonsterAbilityTrigger.Think && !source.IsHurt()) + if (trigger == MonsterAbilityTrigger.Think) { - var fieldUp = _inactiveField?.Remove(source) == true; - if (fieldUp) + if (CanUseField(source) && _inactiveField?.Remove(source) == true) { + // Field going up! source.FixedParticles(0, 10, 0, 0x2530, EffectLayer.Waist); if (_inactiveField?.Count == 0) @@ -36,6 +40,15 @@ public class MagicalBarrier : MonsterAbility } } } + else if (trigger is MonsterAbilityTrigger.TakeSpellDamage && !CanUseField(source)) + { + _inactiveField ??= []; + if (_inactiveField.Add(source)) + { + // TODO: message and effect when field turns down; cannot be verified on OSI due to a bug + source.FixedParticles(0x3735, 1, 30, 0x251F, EffectLayer.Waist); + } + } base.Trigger(trigger, source, target); } @@ -53,22 +66,13 @@ public class MagicalBarrier : MonsterAbility public override void AlterSpellDamageFrom(BaseCreature source, Mobile target, ref int damage) { - var canUseField = source.Hits >= source.HitsMax * 9 / 10; - // If we cannot use the field, deactivate it. - var fieldActive = canUseField ? HasField(source) : _inactiveField.Add(source); - - if (!fieldActive) + if (!HasField(source)) { damage = 0; // no spell damage when the field is down // should there be an effect when spells nullifying is on? source.FixedParticles(0, 10, 0, 0x2522, EffectLayer.Waist); target.SendLocalizedMessage(1114359); // Your attack has no effect on the creature's armor. } - else if (!canUseField) - { - // TODO: message and effect when field turns down; cannot be verified on OSI due to a bug - source.FixedParticles(0x3735, 1, 30, 0x251F, EffectLayer.Waist); - } } public override void Move(BaseCreature source, Direction d) diff --git a/Projects/UOContent/Mobiles/Animals/Misc/PackHorse.cs b/Projects/UOContent/Mobiles/Animals/Misc/PackHorse.cs index ab111f166..ca2d4e3dd 100644 --- a/Projects/UOContent/Mobiles/Animals/Misc/PackHorse.cs +++ b/Projects/UOContent/Mobiles/Animals/Misc/PackHorse.cs @@ -155,7 +155,7 @@ namespace Server.Mobiles if (pack != null) { - Container newPack = new Backpack(); + var newPack = new Backpack(); for (var i = pack.Items.Count - 1; i >= 0; --i) { diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index dc7e91ceb..ed37b7be1 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -448,6 +448,8 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public bool IsPrisoner { get; set; } + public virtual bool FollowsAcquireRules => true; + protected DateTime SummonEnd { get; set; } public virtual Faction FactionAllegiance => null; @@ -4939,7 +4941,7 @@ namespace Server.Mobiles { if (Backpack?.Items.Count > 0) { - Backpack b = new CreatureBackpack(Name); + var b = new CreatureBackpack(Name); var list = new List(Backpack.Items); foreach (var item in list) diff --git a/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs b/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs index e1719b94b..895e0724f 100644 --- a/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs +++ b/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs @@ -1,4 +1,3 @@ -using System; using ModernUO.Serialization; using Server.Items; @@ -7,17 +6,13 @@ namespace Server.Mobiles; [SerializationGenerator(0, false)] public partial class ArcherGuard : BaseGuard { - private Timer _attackTimer; - private Timer _idleTimer; + private bool _shooting; [Constructible] public ArcherGuard(Mobile target = null) : base(target) { InitStats(100, 125, 25); - Title = "the guard"; - SpeechHue = Utility.RandomDyedHue(); - Hue = Race.Human.RandomSkinHue(); if (Female = Utility.RandomBool()) @@ -49,15 +44,15 @@ public partial class ArcherGuard : BaseGuard AddItem(bow); - Container pack = new Backpack(); + var pack = new Backpack + { + Movable = false + }; - pack.Movable = false; - - var arrows = new Arrow(250); - - arrows.LootType = LootType.Newbied; - - pack.DropItem(arrows); + pack.DropItem(new Arrow(250) + { + LootType = LootType.Newbied + }); pack.DropItem(new Gold(10, 25)); AddItem(pack); @@ -107,34 +102,22 @@ public partial class ArcherGuard : BaseGuard Say(500131); // Thou wilt regret thine actions, swine! } - if (_attackTimer != null) - { - _attackTimer.Stop(); - _attackTimer = null; - } - - if (_idleTimer != null) - { - _idleTimer.Stop(); - _idleTimer = null; - } + AttackTimer = null; + IdleTimer = null; if (_focus != null) { - _attackTimer = new AttackTimer(this); - _attackTimer.Start(); - ((AttackTimer)_attackTimer).DoOnTick(); + AttackTimer = new GuardAttackTimer(this); + AttackTimer.Start(); } else { - _idleTimer = new IdleTimer(this); - _idleTimer.Start(); + IdleTimer = new GuardIdleTimer(this); } } - else if (_focus == null && _idleTimer == null) + else if (_focus == null && IdleTimer == null && Spawner != null) { - _idleTimer = new IdleTimer(this); - _idleTimer.Start(); + IdleTimer = new GuardIdleTimer(this); } this.MarkDirty(); @@ -145,245 +128,72 @@ public partial class ArcherGuard : BaseGuard { if (_focus?.Alive == true) { - new AvengeTimer(_focus).Start(); // If a guard dies, three more guards will spawn + new GuardAvengeTimer(_focus).Start(); // If a guard dies, three more guards will spawn } return base.OnBeforeDeath(); } - [AfterDeserialization] - private void AfterDeserialization() + public override void NonLethalAttack(Mobile target) { - if (_focus != null) + if (!InRange(target, 20)) { - _attackTimer = new AttackTimer(this); - _attackTimer.Start(); - } - else - { - _idleTimer = new IdleTimer(this); - _idleTimer.Start(); - } - } - - public override void OnAfterDelete() - { - if (_attackTimer != null) - { - _attackTimer.Stop(); - _attackTimer = null; + _shooting = false; + Focus = null; + return; } - if (_idleTimer != null) + if (!InLOS(target)) { - _idleTimer.Stop(); - _idleTimer = null; + _shooting = false; + TeleportTo(this, target.Location); + return; } - base.OnAfterDelete(); - } - - private class AvengeTimer : Timer - { - private readonly Mobile m_Focus; - - // After 2.5 seconds, one guard will spawn every 1.0 second, three times - public AvengeTimer(Mobile focus) : base(TimeSpan.FromSeconds(2.5), TimeSpan.FromSeconds(1.0), 3) => - m_Focus = focus; - - protected override void OnTick() + if (!CanSee(target)) { - Spawn(m_Focus, m_Focus, 1, true); - } - } + _shooting = false; - private class AttackTimer : Timer - { - private readonly ArcherGuard m_Owner; - // private bool m_Shooting; - - public AttackTimer(ArcherGuard owner) : base(TimeSpan.FromSeconds(0.25), TimeSpan.FromSeconds(0.1)) => - m_Owner = owner; - - public void DoOnTick() - { - OnTick(); - } - - protected override void OnTick() - { - if (m_Owner.Deleted) + if (InRange(target, 2)) { - Stop(); - return; - } - - m_Owner.Criminal = false; - m_Owner.Kills = 0; - m_Owner.Stam = m_Owner.StamMax; - - var target = m_Owner.Focus; - - if (target != null && (target.Deleted || !target.Alive || !m_Owner.CanBeHarmful(target))) - { - m_Owner.Focus = null; - Stop(); - return; - } - - if (m_Owner.Weapon is Fists) - { - m_Owner.Kill(); - Stop(); - return; - } - - if (target != null && m_Owner.Combatant != target) - { - m_Owner.Combatant = target; - } - - if (target == null) - { - Stop(); - } - else - { - // - TeleportTo(target); - target.BoltEffect(0); - - if (target is BaseCreature creature) + if (UseSkill(SkillName.DetectHidden)) { - creature.NoKillAwards = true; + Say("Reveal!"); } - - target.Damage(target.HitsMax, m_Owner); - target.Kill(); // just in case, maybe Damage is overridden on some shard - - if (target.Corpse != null && !target.Player) - { - target.Corpse.Delete(); - } - - m_Owner.Focus = null; - Stop(); - } // - - /*else if (!m_Owner.InRange( target, 20 )) - { - m_Shooting = false; - m_Owner.Focus = null; } - else if (!m_Owner.InLOS( target )) + else if (!Move(GetDirectionTo(target) | Direction.Running) && OutOfMaxDistance(target)) { - m_Shooting = false; - TeleportTo( target ); + TeleportTo(this, target.Location); } - else if (!m_Owner.CanSee( target )) - { - m_Shooting = false; - if (!m_Owner.InRange( target, 2 )) - { - if (!m_Owner.Move( m_Owner.GetDirectionTo( target ) | Direction.Running ) && OutOfMaxDistance( target )) - TeleportTo( target ); - } - else - { - if (!m_Owner.UseSkill( SkillName.DetectHidden ) && Utility.Random( 50 ) == 0) - m_Owner.Say( "Reveal!" ); - } - } - else - { - if (m_Shooting && (TimeToSpare() || OutOfMaxDistance( target ))) - m_Shooting = false; - else if (!m_Shooting && InMinDistance( target )) - m_Shooting = true; - - if (!m_Shooting) - { - if (m_Owner.InRange( target, 1 )) - { - if (!m_Owner.Move( (Direction)(m_Owner.GetDirectionTo( target ) - 4) | Direction.Running ) && OutOfMaxDistance( target )) - TeleportTo( target ); // Too close, move away - } - else if (!m_Owner.InRange( target, 2 )) - { - if (!m_Owner.Move( m_Owner.GetDirectionTo( target ) | Direction.Running ) && OutOfMaxDistance( target )) - TeleportTo( target ); - } - } - }*/ + return; } - private bool TimeToSpare() => m_Owner.NextCombatTime - Core.TickCount > 1000; - - private bool OutOfMaxDistance(IPoint2D target) => !m_Owner.InRange(target, m_Owner.Weapon.MaxRange); - - private bool InMinDistance(IPoint2D target) => m_Owner.InRange(target, 4); - - private void TeleportTo(IEntity target) + if (_shooting) { - var from = m_Owner.Location; - var to = target.Location; + if (TimeToSpare() || OutOfMaxDistance(target)) + { + _shooting = false; + } - m_Owner.Location = to; + return; + } - Effects.SendLocationParticles( - EffectItem.Create(from, m_Owner.Map, EffectItem.DefaultDuration), - 0x3728, - 10, - 10, - 2023 - ); - Effects.SendLocationParticles( - EffectItem.Create(to, m_Owner.Map, EffectItem.DefaultDuration), - 0x3728, - 10, - 10, - 5023 - ); + if (InRange(target, 1) && !Move(GetDirectionTo(target) - 4 | Direction.Running) && OutOfMaxDistance(target)) + { + TeleportTo(this, target.Location); + return; + } - m_Owner.PlaySound(0x1FE); + if (InMinDistance(target)) + { + _shooting = true; } } - private class IdleTimer : Timer - { - private readonly ArcherGuard m_Owner; - private int m_Stage; + private bool TimeToSpare() => NextCombatTime - Core.TickCount > 1000; - public IdleTimer(ArcherGuard owner) : base(TimeSpan.FromSeconds(2.0), TimeSpan.FromSeconds(2.5)) => - m_Owner = owner; + private bool OutOfMaxDistance(IPoint2D target) => !InRange(target, Weapon.MaxRange); - protected override void OnTick() - { - if (m_Owner.Deleted) - { - Stop(); - return; - } - - if (m_Stage++ % 4 == 0 || !m_Owner.Move(m_Owner.Direction)) - { - m_Owner.Direction = (Direction)Utility.Random(8); - } - - if (m_Stage > 16) - { - Effects.SendLocationParticles( - EffectItem.Create(m_Owner.Location, m_Owner.Map, EffectItem.DefaultDuration), - 0x3728, - 10, - 10, - 2023 - ); - m_Owner.PlaySound(0x1FE); - - m_Owner.Delete(); - } - } - } + private bool InMinDistance(IPoint2D target) => InRange(target, 4); } diff --git a/Projects/UOContent/Mobiles/Guards/BaseGuard.cs b/Projects/UOContent/Mobiles/Guards/BaseGuard.cs index 25d302394..6dd80edb1 100644 --- a/Projects/UOContent/Mobiles/Guards/BaseGuard.cs +++ b/Projects/UOContent/Mobiles/Guards/BaseGuard.cs @@ -1,3 +1,5 @@ +using System; +using System.Runtime.CompilerServices; using ModernUO.Serialization; using Server.Items; @@ -6,8 +8,74 @@ namespace Server.Mobiles; [SerializationGenerator(0)] public abstract partial class BaseGuard : Mobile { + public static bool GuardsInstantKill { get; private set; } + public static void Configure() + { + GuardsInstantKill = ServerConfiguration.GetSetting("guards.instantKill", true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Spawn(Mobile caller, Mobile target, int amount = 1, bool onlyAdditional = false) => + Spawn(caller.Region, target, amount, onlyAdditional); + + public static void Spawn(Region region, Mobile target, int amount = 1, bool onlyAdditional = false) + { + if (target?.Deleted != false) + { + return; + } + + foreach (var g in target.GetMobilesInRange(15)) + { + if (g.Focus == null) // idling + { + g.Focus = target; + + --amount; + } + else if (g.Focus == target && !onlyAdditional) + { + --amount; + } + } + + while (amount-- > 0) + { + region.MakeGuard(target); + } + } + + public static void TeleportTo(Mobile source, Point3D to) + { + Effects.SendLocationParticles( + EffectItem.Create(source.Location, source.Map, EffectItem.DefaultDuration), + 0x3728, + 10, + 10, + 2023 + ); + + source.Location = to; + + Effects.SendLocationParticles( + EffectItem.Create(to, source.Map, EffectItem.DefaultDuration), + 0x3728, + 10, + 10, + 5023 + ); + + source.PlaySound(0x1FE); + } + + + private GuardIdleTimer _idleTimer; + private GuardAttackTimer _attackTimer; + public BaseGuard(Mobile target) { + Title = "the guard"; + if (target != null) { Location = target.Location; @@ -20,39 +88,40 @@ public abstract partial class BaseGuard : Mobile 10, 5023 ); + + Focus = target; + } + } + + protected GuardAttackTimer AttackTimer + { + get => _attackTimer; + set + { + _attackTimer?.Stop(); + _attackTimer = value; + _attackTimer?.Start(); + } + } + + protected GuardIdleTimer IdleTimer + { + get => _idleTimer; + set + { + _idleTimer?.Stop(); + _idleTimer = value; + _idleTimer?.Start(); } } public abstract Mobile Focus { get; set; } - public static void Spawn(Mobile caller, Mobile target, int amount = 1, bool onlyAdditional = false) + public override void OnAfterDelete() { - if (target?.Deleted != false) - { - return; - } - - foreach (var m in target.GetMobilesInRange(15)) - { - if (m is BaseGuard g) - { - if (g.Focus == null) // idling - { - g.Focus = target; - - --amount; - } - else if (g.Focus == target && !onlyAdditional) - { - --amount; - } - } - } - - while (amount-- > 0) - { - caller.Region.MakeGuard(target); - } + AttackTimer = null; + IdleTimer = null; + base.OnAfterDelete(); } public override bool OnBeforeDeath() @@ -66,9 +135,148 @@ public abstract partial class BaseGuard : Mobile ); PlaySound(0x1FE); - Delete(); - return false; } + + public abstract void NonLethalAttack(Mobile target); + + [AfterDeserialization] + private void AfterDeserialization() + { + if (Focus != null) + { + AttackTimer = new GuardAttackTimer(this); + } + else + { + IdleTimer = new GuardIdleTimer(this); + } + } +} + +public class GuardAvengeTimer : Timer +{ + private readonly Mobile _focus; + + public GuardAvengeTimer(Mobile focus) : base(TimeSpan.FromSeconds(2.5), TimeSpan.FromSeconds(1.0), 3) => + _focus = focus; + + protected override void OnTick() => BaseGuard.Spawn(_focus, _focus, 1, true); +} + +public class GuardIdleTimer : Timer +{ + private readonly BaseGuard _owner; + private int m_Stage; + + public GuardIdleTimer(BaseGuard owner) : base(TimeSpan.FromSeconds(2.0), TimeSpan.FromSeconds(2.5)) => + _owner = owner; + + protected override void OnTick() + { + if (_owner.Deleted) + { + Stop(); + return; + } + + if (m_Stage++ % 4 == 0 || !_owner.Move(_owner.Direction)) + { + _owner.Direction = (Direction)Utility.Random(8); + } + + if (m_Stage > 16) + { + Effects.SendLocationParticles( + EffectItem.Create(_owner.Location, _owner.Map, EffectItem.DefaultDuration), + 0x3728, + 10, + 10, + 2023 + ); + _owner.PlaySound(0x1FE); + + if (_owner.Spawner == null) + { + _owner.Delete(); + } + else + { + BaseGuard.TeleportTo(_owner, _owner.Spawner.HomeLocation); + } + + Stop(); + } + } +} + +public class GuardAttackTimer : Timer +{ + private readonly BaseGuard _owner; + + public GuardAttackTimer(BaseGuard owner) : base(TimeSpan.FromSeconds(0.25), TimeSpan.FromSeconds(0.1)) => + _owner = owner; + + public void DoOnTick() + { + OnTick(); + } + + protected override void OnTick() + { + if (_owner.Deleted) + { + Stop(); + return; + } + + _owner.Criminal = false; + _owner.Kills = 0; + _owner.Stam = _owner.StamMax; + + var target = _owner.Focus; + + if (target != null && (target.Deleted || !target.Alive || !_owner.CanBeHarmful(target))) + { + _owner.Focus = null; + Stop(); + return; + } + + if (target != null && _owner.Combatant != target) + { + _owner.Combatant = target; + } + + if (target == null) + { + Stop(); + } + else if (BaseGuard.GuardsInstantKill) + { + BaseGuard.TeleportTo(_owner, target.Location); + target.BoltEffect(0); + + if (target is BaseCreature creature) + { + creature.NoKillAwards = true; + } + + target.Damage(target.HitsMax, _owner); + target.Kill(); // just in case, maybe Damage is overridden on some shard + + if (target.Corpse != null && !target.Player) + { + target.Corpse.Delete(); + } + + _owner.Focus = null; + Stop(); + } + else + { + _owner.NonLethalAttack(target); + } + } } diff --git a/Projects/UOContent/Mobiles/Guards/WarriorGuard.cs b/Projects/UOContent/Mobiles/Guards/WarriorGuard.cs index c818fa0aa..c402980b4 100644 --- a/Projects/UOContent/Mobiles/Guards/WarriorGuard.cs +++ b/Projects/UOContent/Mobiles/Guards/WarriorGuard.cs @@ -1,4 +1,3 @@ -using System; using ModernUO.Serialization; using Server.Items; @@ -7,17 +6,11 @@ namespace Server.Mobiles; [SerializationGenerator(0, false)] public partial class WarriorGuard : BaseGuard { - private Timer _attackTimer; - private Timer _idleTimer; - [Constructible] public WarriorGuard(Mobile target = null) : base(target) { - InitStats(1000, 1000, 1000); - Title = "the guard"; - + InitStats(100, 125, 25); SpeechHue = Utility.RandomDyedHue(); - Hue = Race.Human.RandomSkinHue(); if (Female = Utility.RandomBool()) @@ -73,12 +66,12 @@ public partial class WarriorGuard : BaseGuard AddItem(weapon); - Container pack = new Backpack(); - - pack.Movable = false; + var pack = new Backpack + { + Movable = false + }; pack.DropItem(new Gold(10, 25)); - AddItem(pack); Skills.Anatomy.Base = 120.0; @@ -88,7 +81,6 @@ public partial class WarriorGuard : BaseGuard Skills.DetectHidden.Base = 100.0; NextCombatTime = Core.TickCount + 500; - Focus = target; } [SerializableProperty(0)] @@ -126,34 +118,22 @@ public partial class WarriorGuard : BaseGuard Say(500131); // Thou wilt regret thine actions, swine! } - if (_attackTimer != null) - { - _attackTimer.Stop(); - _attackTimer = null; - } - - if (_idleTimer != null) - { - _idleTimer.Stop(); - _idleTimer = null; - } + AttackTimer = null; + IdleTimer = null; if (_focus != null) { - _attackTimer = new AttackTimer(this); - _attackTimer.Start(); - ((AttackTimer)_attackTimer).DoOnTick(); + AttackTimer = new GuardAttackTimer(this); + AttackTimer.Start(); } else { - _idleTimer = new IdleTimer(this); - _idleTimer.Start(); + IdleTimer = new GuardIdleTimer(this); } } - else if (_focus == null && _idleTimer == null) + else if (_focus == null && IdleTimer == null && Spawner != null) { - _idleTimer = new IdleTimer(this); - _idleTimer.Start(); + IdleTimer = new GuardIdleTimer(this); } this.MarkDirty(); @@ -164,209 +144,32 @@ public partial class WarriorGuard : BaseGuard { if (_focus?.Alive == true) { - new AvengeTimer(_focus).Start(); // If a guard dies, three more guards will spawn + new GuardAvengeTimer(_focus).Start(); // If a guard dies, three more guards will spawn } return base.OnBeforeDeath(); } - [AfterDeserialization] - private void AfterDeserialization() + public override void NonLethalAttack(Mobile target) { - if (_focus != null) + if (!InRange(target, 20)) { - _attackTimer = new AttackTimer(this); - _attackTimer.Start(); + Focus = null; } - else + else if (!InRange(target, 10) || !InLOS(target)) { - _idleTimer = new IdleTimer(this); - _idleTimer.Start(); + TeleportTo(this, target.Location); } - } - - public override void OnAfterDelete() - { - if (_attackTimer != null) + else if (!InRange(target, 1)) { - _attackTimer.Stop(); - _attackTimer = null; + if (!Move(GetDirectionTo(target) | Direction.Running)) + { + TeleportTo(this, target.Location); + } } - - if (_idleTimer != null) + else if (!CanSee(target) && UseSkill(SkillName.DetectHidden)) { - _idleTimer.Stop(); - _idleTimer = null; - } - - base.OnAfterDelete(); - } - - private class AvengeTimer : Timer - { - private readonly Mobile m_Focus; - - public AvengeTimer(Mobile focus) : base(TimeSpan.FromSeconds(2.5), TimeSpan.FromSeconds(1.0), 3) => - m_Focus = focus; - - protected override void OnTick() - { - Spawn(m_Focus, m_Focus, 1, true); - } - } - - private class AttackTimer : Timer - { - private readonly WarriorGuard m_Owner; - - public AttackTimer(WarriorGuard owner) : base(TimeSpan.FromSeconds(0.25), TimeSpan.FromSeconds(0.1)) => - m_Owner = owner; - - public void DoOnTick() - { - OnTick(); - } - - protected override void OnTick() - { - if (m_Owner.Deleted) - { - Stop(); - return; - } - - m_Owner.Criminal = false; - m_Owner.Kills = 0; - m_Owner.Stam = m_Owner.StamMax; - - var target = m_Owner.Focus; - - if (target != null && (target.Deleted || !target.Alive || !m_Owner.CanBeHarmful(target))) - { - m_Owner.Focus = null; - Stop(); - return; - } - - if (m_Owner.Weapon is Fists) - { - m_Owner.Kill(); - Stop(); - return; - } - - if (target != null && m_Owner.Combatant != target) - { - m_Owner.Combatant = target; - } - - if (target == null) - { - Stop(); - } - else - { - // - TeleportTo(target); - target.BoltEffect(0); - - if (target is BaseCreature creature) - { - creature.NoKillAwards = true; - } - - target.Damage(target.HitsMax, m_Owner); - target.Kill(); // just in case, maybe Damage is overridden on some shard - - if (target.Corpse != null && !target.Player) - { - target.Corpse.Delete(); - } - - m_Owner.Focus = null; - Stop(); - } // - - /*else if (!m_Owner.InRange( target, 20 )) - { - m_Owner.Focus = null; - } - else if (!m_Owner.InRange( target, 10 ) || !m_Owner.InLOS( target )) - { - TeleportTo( target ); - } - else if (!m_Owner.InRange( target, 1 )) - { - if (!m_Owner.Move( m_Owner.GetDirectionTo( target ) | Direction.Running )) - TeleportTo( target ); - } - else if (!m_Owner.CanSee( target )) - { - if (!m_Owner.UseSkill( SkillName.DetectHidden ) && Utility.Random( 50 ) == 0) - m_Owner.Say( "Reveal!" ); - }*/ - } - - private void TeleportTo(Mobile target) - { - var from = m_Owner.Location; - var to = target.Location; - - m_Owner.Location = to; - - Effects.SendLocationParticles( - EffectItem.Create(from, m_Owner.Map, EffectItem.DefaultDuration), - 0x3728, - 10, - 10, - 2023 - ); - Effects.SendLocationParticles( - EffectItem.Create(to, m_Owner.Map, EffectItem.DefaultDuration), - 0x3728, - 10, - 10, - 5023 - ); - - m_Owner.PlaySound(0x1FE); - } - } - - private class IdleTimer : Timer - { - private readonly WarriorGuard m_Owner; - private int m_Stage; - - public IdleTimer(WarriorGuard owner) : base(TimeSpan.FromSeconds(2.0), TimeSpan.FromSeconds(2.5)) => - m_Owner = owner; - - protected override void OnTick() - { - if (m_Owner.Deleted) - { - Stop(); - return; - } - - if (m_Stage++ % 4 == 0 || !m_Owner.Move(m_Owner.Direction)) - { - m_Owner.Direction = (Direction)Utility.Random(8); - } - - if (m_Stage > 16) - { - Effects.SendLocationParticles( - EffectItem.Create(m_Owner.Location, m_Owner.Map, EffectItem.DefaultDuration), - 0x3728, - 10, - 10, - 2023 - ); - m_Owner.PlaySound(0x1FE); - - m_Owner.Delete(); - } + Say("Reveal!"); } } } diff --git a/Projects/UOContent/Mobiles/Monsters/Elemental/Melee/EarthElemental.cs b/Projects/UOContent/Mobiles/Monsters/Elemental/Melee/EarthElemental.cs index f8f725f7b..35cf273af 100644 --- a/Projects/UOContent/Mobiles/Monsters/Elemental/Melee/EarthElemental.cs +++ b/Projects/UOContent/Mobiles/Monsters/Elemental/Melee/EarthElemental.cs @@ -40,9 +40,10 @@ namespace Server.Mobiles PackItem(new FertileDirt(Utility.RandomMinMax(1, 4))); PackItem(new MandrakeRoot()); - Item ore = new IronOre(5); - ore.ItemID = 0x19B7; - PackItem(ore); + PackItem(new IronOre(5) + { + ItemID = 0x19B7 + }); } public override string CorpseName => "an earth elemental corpse"; diff --git a/Projects/UOContent/Mobiles/Monsters/Elemental/Melee/SnowElemental.cs b/Projects/UOContent/Mobiles/Monsters/Elemental/Melee/SnowElemental.cs index 60005a74c..3884a6c03 100644 --- a/Projects/UOContent/Mobiles/Monsters/Elemental/Melee/SnowElemental.cs +++ b/Projects/UOContent/Mobiles/Monsters/Elemental/Melee/SnowElemental.cs @@ -39,9 +39,10 @@ namespace Server.Mobiles VirtualArmor = 50; PackItem(new BlackPearl(3)); - Item ore = new IronOre(3); - ore.ItemID = 0x19B8; - PackItem(ore); + PackItem(new IronOre(3) + { + ItemID = 0x19B8 + }); } public override string CorpseName => "a snow elemental corpse"; diff --git a/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/OrcBrute.cs b/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/OrcBrute.cs index 45541153c..317df7658 100644 --- a/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/OrcBrute.cs +++ b/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/OrcBrute.cs @@ -38,9 +38,10 @@ namespace Server.Mobiles VirtualArmor = 50; - Item ore = new ShadowIronOre(25); - ore.ItemID = 0x19B9; - PackItem(ore); + PackItem(new ShadowIronOre(25) + { + ItemID = 0x19B9 + }); PackItem(new IronIngot(10)); if (Utility.RandomDouble() < 0.05) diff --git a/Projects/UOContent/Mobiles/Monsters/LBR/Jukas/JukaLord.cs b/Projects/UOContent/Mobiles/Monsters/LBR/Jukas/JukaLord.cs index 6685790ba..eb276888a 100644 --- a/Projects/UOContent/Mobiles/Monsters/LBR/Jukas/JukaLord.cs +++ b/Projects/UOContent/Mobiles/Monsters/LBR/Jukas/JukaLord.cs @@ -40,7 +40,7 @@ namespace Server.Mobiles VirtualArmor = 28; - Container pack = new Backpack(); + var pack = new Backpack(); pack.DropItem(new Arrow(Utility.RandomMinMax(25, 35))); pack.DropItem(new Arrow(Utility.RandomMinMax(25, 35))); diff --git a/Projects/UOContent/Mobiles/Monsters/LBR/Jukas/JukaMage.cs b/Projects/UOContent/Mobiles/Monsters/LBR/Jukas/JukaMage.cs index abf2c39dc..a0d486535 100644 --- a/Projects/UOContent/Mobiles/Monsters/LBR/Jukas/JukaMage.cs +++ b/Projects/UOContent/Mobiles/Monsters/LBR/Jukas/JukaMage.cs @@ -45,7 +45,7 @@ namespace Server.Mobiles VirtualArmor = 16; - Container bag = new Bag(); + var bag = new Bag(); var count = Utility.RandomMinMax(10, 20); diff --git a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerCaptain.cs b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerCaptain.cs index 3af690d48..d4b8eaf82 100644 --- a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerCaptain.cs +++ b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerCaptain.cs @@ -42,7 +42,7 @@ namespace Server.Mobiles VirtualArmor = 28; - Container pack = new Backpack(); + var pack = new Backpack(); pack.DropItem(new Bolt(Utility.RandomMinMax(10, 20))); pack.DropItem(new Bolt(Utility.RandomMinMax(10, 20))); @@ -59,7 +59,7 @@ namespace Server.Mobiles } ); - Container bag = new Bag(); + var bag = new Bag(); var count = Utility.RandomMinMax(10, 20); diff --git a/Projects/UOContent/Mobiles/Monsters/ML/Prism of Light/Protector.cs b/Projects/UOContent/Mobiles/Monsters/ML/Prism of Light/Protector.cs index e0e11287f..4fbe38291 100644 --- a/Projects/UOContent/Mobiles/Monsters/ML/Prism of Light/Protector.cs +++ b/Projects/UOContent/Mobiles/Monsters/ML/Prism of Light/Protector.cs @@ -42,17 +42,17 @@ namespace Server.Mobiles Fame = 10000; Karma = -10000; - Item boots = new ThighBoots(); - boots.Movable = false; - boots.Hue = Utility.Random(2); - - var shroud = new Item(0x204E); - shroud.Layer = Layer.OuterTorso; - shroud.Movable = false; - shroud.Hue = Utility.Random(2); - - AddItem(boots); - AddItem(shroud); + AddItem(new ThighBoots + { + Movable = false, + Hue = Utility.Random(2) + }); + AddItem(new Item(0x204E) + { + Layer = Layer.OuterTorso, + Movable = false, + Hue = Utility.Random(2) + }); } public override string CorpseName => "a human corpse"; diff --git a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/BladeSpirits.cs b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/BladeSpirits.cs index 85b5d0671..100e115d7 100644 --- a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/BladeSpirits.cs +++ b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/BladeSpirits.cs @@ -45,7 +45,7 @@ namespace Server.Mobiles public override string CorpseName => "a blade spirit corpse"; public override string DefaultName => "a blade spirit"; - + public override bool DeleteCorpseOnDeath => Core.AOS; public override bool IsHouseSummonable => true; @@ -55,6 +55,8 @@ namespace Server.Mobiles public override bool BleedImmune => true; public override Poison PoisonImmune => Poison.Lethal; + public override bool FollowsAcquireRules => Core.AOS || !Summoned || SummonMaster?.Player != true || Map != Map.Felucca; + public override double GetFightModeRanking(Mobile m, FightMode acqType, bool bPlayerOnly) => (m.Str + m.Skills.Tactics.Value) / Math.Max(GetDistanceToSqrt(m), 1.0); diff --git a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/EnergyVortex.cs b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/EnergyVortex.cs index a6ab88261..b94d2650c 100644 --- a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/EnergyVortex.cs +++ b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/EnergyVortex.cs @@ -63,6 +63,8 @@ namespace Server.Mobiles public override bool BleedImmune => true; public override Poison PoisonImmune => Poison.Lethal; + public override bool FollowsAcquireRules => Core.AOS || !Summoned || SummonMaster?.Player != true || Map != Map.Felucca; + public override double GetFightModeRanking(Mobile m, FightMode acqType, bool bPlayerOnly) => (m.Int + m.Skills.Magery.Value) / Math.Max(GetDistanceToSqrt(m), 1.0); diff --git a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeast.cs b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeast.cs index cb469a55a..8c7984360 100644 --- a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeast.cs +++ b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeast.cs @@ -124,9 +124,11 @@ namespace Server.Mobiles { if (Map != null && caster != this && Utility.RandomDouble() < 0.25) { - BaseCreature spawn = new PlagueSpawn(this); + var spawn = new PlagueSpawn(this) + { + Team = Team + }; - spawn.Team = Team; spawn.MoveToWorld(Location, Map); spawn.Combatant = caster; @@ -140,9 +142,11 @@ namespace Server.Mobiles { if (Map != null && attacker != this && Utility.RandomDouble() < 0.25) { - BaseCreature spawn = new PlagueSpawn(this); + var spawn = new PlagueSpawn(this) + { + Team = Team + }; - spawn.Team = Team; spawn.MoveToWorld(Location, Map); spawn.Combatant = attacker; diff --git a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/AgapiteElemental.cs b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/AgapiteElemental.cs index bdf45058f..60db2e920 100644 --- a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/AgapiteElemental.cs +++ b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/AgapiteElemental.cs @@ -37,9 +37,10 @@ namespace Server.Mobiles VirtualArmor = 32; - Item ore = new AgapiteOre(oreAmount); - ore.ItemID = 0x19B9; - PackItem(ore); + PackItem(new AgapiteOre(oreAmount) + { + ItemID = 0x19B9 + }); } public override string CorpseName => "an ore elemental corpse"; diff --git a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/BronzeElemental.cs b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/BronzeElemental.cs index e90e95bb7..ecd9df804 100644 --- a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/BronzeElemental.cs +++ b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/BronzeElemental.cs @@ -38,9 +38,10 @@ namespace Server.Mobiles VirtualArmor = 29; - Item ore = new BronzeOre(oreAmount); - ore.ItemID = 0x19B9; - PackItem(ore); + PackItem(new BronzeOre(oreAmount) + { + ItemID = 0x19B9 + }); } public override string CorpseName => "an ore elemental corpse"; diff --git a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/CopperElemental.cs b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/CopperElemental.cs index 0adc81b6e..d2388302f 100644 --- a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/CopperElemental.cs +++ b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/CopperElemental.cs @@ -37,9 +37,10 @@ namespace Server.Mobiles VirtualArmor = 26; - Item ore = new CopperOre(oreAmount); - ore.ItemID = 0x19B9; - PackItem(ore); + PackItem(new CopperOre(oreAmount) + { + ItemID = 0x19B9 + }); } public override string CorpseName => "an ore elemental corpse"; diff --git a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/DullCopperElemental.cs b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/DullCopperElemental.cs index e8f94d462..c2fd84e75 100644 --- a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/DullCopperElemental.cs +++ b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/DullCopperElemental.cs @@ -37,9 +37,10 @@ namespace Server.Mobiles VirtualArmor = 20; - Item ore = new DullCopperOre(oreAmount); - ore.ItemID = 0x19B9; - PackItem(ore); + PackItem(new DullCopperOre(oreAmount) + { + ItemID = 0x19B9 + }); } public override string CorpseName => "an ore elemental corpse"; diff --git a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/GoldenElemental.cs b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/GoldenElemental.cs index 35903c3fe..845a1f1ca 100644 --- a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/GoldenElemental.cs +++ b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/GoldenElemental.cs @@ -37,9 +37,10 @@ namespace Server.Mobiles VirtualArmor = 60; - Item ore = new GoldOre(oreAmount); - ore.ItemID = 0x19B9; - PackItem(ore); + PackItem(new GoldOre(oreAmount) + { + ItemID = 0x19B9 + }); } public override string CorpseName => "an ore elemental corpse"; diff --git a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/ShadowIronElemental.cs b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/ShadowIronElemental.cs index edfeffeb8..e68c6f4f5 100644 --- a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/ShadowIronElemental.cs +++ b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/ShadowIronElemental.cs @@ -37,9 +37,10 @@ namespace Server.Mobiles VirtualArmor = 23; - Item ore = new ShadowIronOre(oreAmount); - ore.ItemID = 0x19B9; - PackItem(ore); + PackItem(new ShadowIronOre(oreAmount) + { + ItemID = 0x19B9 + }); } public override string CorpseName => "an ore elemental corpse"; diff --git a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/ValoriteElemental.cs b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/ValoriteElemental.cs index 64c099969..2480d3a9c 100644 --- a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/ValoriteElemental.cs +++ b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/ValoriteElemental.cs @@ -40,9 +40,10 @@ namespace Server.Mobiles VirtualArmor = 38; - Item ore = new ValoriteOre(oreAmount); - ore.ItemID = 0x19B9; - PackItem(ore); + PackItem(new ValoriteOre(oreAmount) + { + ItemID = 0x19B9 + }); } public override string CorpseName => "an ore elemental corpse"; diff --git a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/VeriteElemental.cs b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/VeriteElemental.cs index 306c07d06..9b1a1e547 100644 --- a/Projects/UOContent/Mobiles/Monsters/Ore Elementals/VeriteElemental.cs +++ b/Projects/UOContent/Mobiles/Monsters/Ore Elementals/VeriteElemental.cs @@ -38,9 +38,10 @@ namespace Server.Mobiles VirtualArmor = 35; - Item ore = new VeriteOre(oreAmount); - ore.ItemID = 0x19B9; - PackItem(ore); + PackItem(new VeriteOre(oreAmount) + { + ItemID = 0x19B9 + }); } public override string CorpseName => "an ore elemental corpse"; diff --git a/Projects/UOContent/Mobiles/Monsters/Summons/SummonedAirElemental.cs b/Projects/UOContent/Mobiles/Monsters/Summons/SummonedAirElemental.cs index 28a225e78..e6cc90190 100644 --- a/Projects/UOContent/Mobiles/Monsters/Summons/SummonedAirElemental.cs +++ b/Projects/UOContent/Mobiles/Monsters/Summons/SummonedAirElemental.cs @@ -1,4 +1,3 @@ -using System; using ModernUO.Serialization; namespace Server.Mobiles diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index 9da05f1c1..fe2e6a05b 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -2360,7 +2360,7 @@ namespace Server.Mobiles if (Alive && !wasAlive) { - Item deathRobe = new DeathRobe(); + var deathRobe = new DeathRobe(); if (!EquipItem(deathRobe)) { @@ -2721,7 +2721,7 @@ namespace Server.Mobiles } } - private static void SendToStaffMessage(Mobile from, string text) + private static void SendToStaffMessage(PlayerMobile from, string text) { Span buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(text)].InitializePacket(); @@ -4733,7 +4733,7 @@ namespace Server.Mobiles for (int i = _page * 4, y = 72; i < (_page + 1) * 4 && i < _items.Length; ++i, y += 75) { var item = _items[i]; - var b = ItemBounds.Table[item.ItemID]; + var b = ItemBounds.Bounds[item.ItemID]; builder.AddImageTiledButton( 40, diff --git a/Projects/UOContent/Mobiles/Special/Harrower.cs b/Projects/UOContent/Mobiles/Special/Harrower.cs index 9b69aa3c7..4b2e31c01 100644 --- a/Projects/UOContent/Mobiles/Special/Harrower.cs +++ b/Projects/UOContent/Mobiles/Special/Harrower.cs @@ -343,7 +343,7 @@ public partial class Harrower : BaseCreature for (var i = 0; i < _tentacles.Count; ++i) { - Mobile m = _tentacles[i]; + var m = _tentacles[i]; if (!m.Deleted) { diff --git a/Projects/UOContent/Mobiles/Special/LordOaks.cs b/Projects/UOContent/Mobiles/Special/LordOaks.cs index 0387d6353..f8fb6d444 100644 --- a/Projects/UOContent/Mobiles/Special/LordOaks.cs +++ b/Projects/UOContent/Mobiles/Special/LordOaks.cs @@ -9,7 +9,7 @@ namespace Server.Mobiles; public partial class LordOaks : BaseChampion { [SerializableField(0)] - private BaseCreature _queen; + private Silvani _queen; [SerializableField(1)] private bool _spawnedQueen; diff --git a/Projects/UOContent/Mobiles/Townfolk/Actor.cs b/Projects/UOContent/Mobiles/Townfolk/Actor.cs index c13b8b087..375ec8116 100644 --- a/Projects/UOContent/Mobiles/Townfolk/Actor.cs +++ b/Projects/UOContent/Mobiles/Townfolk/Actor.cs @@ -35,12 +35,12 @@ public partial class Actor : BaseCreature Utility.AssignRandomHair(this); - Container pack = new Backpack(); + var pack = new Backpack + { + Movable = false + }; pack.DropItem(new Gold(250, 300)); - - pack.Movable = false; - AddItem(pack); } diff --git a/Projects/UOContent/Mobiles/Townfolk/Artist.cs b/Projects/UOContent/Mobiles/Townfolk/Artist.cs index 9e39817e5..271026e63 100644 --- a/Projects/UOContent/Mobiles/Townfolk/Artist.cs +++ b/Projects/UOContent/Mobiles/Townfolk/Artist.cs @@ -35,12 +35,12 @@ public partial class Artist : BaseCreature Utility.AssignRandomHair(this); - Container pack = new Backpack(); + var pack = new Backpack + { + Movable = false + }; pack.DropItem(new Gold(250, 300)); - - pack.Movable = false; - AddItem(pack); } diff --git a/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs b/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs index 04c6e9311..5c0187da0 100644 --- a/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs +++ b/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using ModernUO.Serialization; using Server.Buffers; @@ -849,7 +848,7 @@ public class EscortDestinationInfo public static void Initialize() { - ICollection list = Map.Felucca.Regions.Values; + var list = Map.Felucca.Regions.Values; if (list.Count == 0) { diff --git a/Projects/UOContent/Mobiles/Townfolk/Gypsy.cs b/Projects/UOContent/Mobiles/Townfolk/Gypsy.cs index ab08cf4a9..a645b8d83 100644 --- a/Projects/UOContent/Mobiles/Townfolk/Gypsy.cs +++ b/Projects/UOContent/Mobiles/Townfolk/Gypsy.cs @@ -42,12 +42,12 @@ public partial class Gypsy : BaseCreature Utility.AssignRandomHair(this); - Container pack = new Backpack(); + var pack = new Backpack + { + Movable = false + }; pack.DropItem(new Gold(250, 300)); - - pack.Movable = false; - AddItem(pack); } diff --git a/Projects/UOContent/Mobiles/Townfolk/HarborMaster.cs b/Projects/UOContent/Mobiles/Townfolk/HarborMaster.cs index 25c35513e..84e1f1d3a 100644 --- a/Projects/UOContent/Mobiles/Townfolk/HarborMaster.cs +++ b/Projects/UOContent/Mobiles/Townfolk/HarborMaster.cs @@ -37,12 +37,12 @@ public partial class HarborMaster : BaseCreature Utility.AssignRandomHair(this); - Container pack = new Backpack(); + var pack = new Backpack + { + Movable = false + }; pack.DropItem(new Gold(250, 300)); - - pack.Movable = false; - AddItem(pack); } diff --git a/Projects/UOContent/Mobiles/Townfolk/Sculptor.cs b/Projects/UOContent/Mobiles/Townfolk/Sculptor.cs index 00f23353c..c669822b7 100644 --- a/Projects/UOContent/Mobiles/Townfolk/Sculptor.cs +++ b/Projects/UOContent/Mobiles/Townfolk/Sculptor.cs @@ -34,12 +34,12 @@ public partial class Sculptor : BaseCreature Utility.AssignRandomHair(this); - Container pack = new Backpack(); + var pack = new Backpack + { + Movable = false + }; pack.DropItem(new Gold(250, 300)); - - pack.Movable = false; - AddItem(pack); } diff --git a/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs b/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs index 94b3eaf2a..a1496d07c 100644 --- a/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs +++ b/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs @@ -1163,7 +1163,7 @@ namespace Server.Mobiles } private static bool ProcessSinglePurchase( - BuyItemResponse buy, IBuyItemInfo bii, List validBuy, + BuyItemResponse buy, GenericBuyInfo bii, List validBuy, ref int controlSlots, ref bool fullPurchase, ref int totalCost ) { @@ -1211,7 +1211,7 @@ namespace Server.Mobiles return true; } - private static void ProcessValidPurchase(int amount, IBuyItemInfo bii, Mobile buyer, Container cont) + private static void ProcessValidPurchase(int amount, GenericBuyInfo bii, Mobile buyer, Container cont) { if (amount > bii.Amount) { diff --git a/Projects/UOContent/Mobiles/Vendors/NPC/Blacksmith.cs b/Projects/UOContent/Mobiles/Vendors/NPC/Blacksmith.cs index 033bd30e0..5d9b39cbd 100644 --- a/Projects/UOContent/Mobiles/Vendors/NPC/Blacksmith.cs +++ b/Projects/UOContent/Mobiles/Vendors/NPC/Blacksmith.cs @@ -32,19 +32,19 @@ namespace Server.Mobiles public override void InitSBInfo() { /*m_SBInfos.Add( new SBSmithTools() ); - + m_SBInfos.Add( new SBMetalShields() ); m_SBInfos.Add( new SBWoodenShields() ); - + m_SBInfos.Add( new SBPlateArmor() ); - + m_SBInfos.Add( new SBHelmetArmor() ); m_SBInfos.Add( new SBChainmailArmor() ); m_SBInfos.Add( new SBRingmailArmor() ); m_SBInfos.Add( new SBAxeWeapon() ); m_SBInfos.Add( new SBPoleArmWeapon() ); m_SBInfos.Add( new SBRangedWeapon() ); - + m_SBInfos.Add( new SBKnifeWeapon() ); m_SBInfos.Add( new SBMaceWeapon() ); m_SBInfos.Add( new SBSpearForkWeapon() ); @@ -62,17 +62,11 @@ namespace Server.Mobiles { base.InitOutfit(); - Item item = Utility.RandomBool() ? null : new RingmailChest(); + Item item = Utility.RandomBool() ? new FullApron() : new RingmailChest(); - if (item != null && !EquipItem(item)) + if (!EquipItem(item)) { item.Delete(); - item = null; - } - - if (item == null) - { - AddItem(new FullApron()); } AddItem(new Bascinet()); diff --git a/Projects/UOContent/Mobiles/Vendors/NPC/Guildmasters/BlacksmithGuildmaster.cs b/Projects/UOContent/Mobiles/Vendors/NPC/Guildmasters/BlacksmithGuildmaster.cs index cfc6baf8c..2bc52b4c7 100644 --- a/Projects/UOContent/Mobiles/Vendors/NPC/Guildmasters/BlacksmithGuildmaster.cs +++ b/Projects/UOContent/Mobiles/Vendors/NPC/Guildmasters/BlacksmithGuildmaster.cs @@ -32,17 +32,11 @@ namespace Server.Mobiles { base.InitOutfit(); - Item item = Utility.RandomBool() ? null : new RingmailChest(); + Item item = Utility.RandomBool() ? new FullApron() : new RingmailChest(); - if (item != null && !EquipItem(item)) + if (!EquipItem(item)) { item.Delete(); - item = null; - } - - if (item == null) - { - AddItem(new FullApron()); } AddItem(new Bascinet()); diff --git a/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs b/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs index 2721fadbd..9cf360aff 100644 --- a/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs +++ b/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs @@ -227,9 +227,10 @@ public partial class PlayerVendor : Mobile public virtual void InitOutfit() { - Item item = new FancyShirt(Utility.RandomNeutralHue()); - item.Layer = Layer.InnerTorso; - AddItem(item); + AddItem(new FancyShirt(Utility.RandomNeutralHue()) + { + Layer = Layer.InnerTorso + }); AddItem(new LongPants(Utility.RandomNeutralHue())); AddItem(new BodySash(Utility.RandomNeutralHue())); AddItem(new Boots(Utility.RandomNeutralHue())); @@ -1232,9 +1233,9 @@ public partial class PlayerVendor : Mobile return; } - var name = text.Trim(); + var name = text.AsSpan().Trim(); - if (!NameVerification.Validate(name, 1, 20, true, true, true, 0, NameVerification.Empty)) + if (!NameVerification.ValidateVendorName(name)) { m_Vendor.SayTo(from, "That name is unacceptable."); return; @@ -1261,9 +1262,9 @@ public partial class PlayerVendor : Mobile return; } - var name = text.Trim(); + var name = text.AsSpan().Trim(); - if (!NameVerification.Validate(name, 1, 20, true, true, true, 0, NameVerification.Empty)) + if (!NameVerification.ValidateVendorName(name)) { m_Vendor.SayTo(from, "That name is unacceptable."); return; diff --git a/Projects/UOContent/Multis/ComponentVerification.cs b/Projects/UOContent/Multis/ComponentVerification.cs index f0fd40d08..9942fc9a0 100644 --- a/Projects/UOContent/Multis/ComponentVerification.cs +++ b/Projects/UOContent/Multis/ComponentVerification.cs @@ -147,17 +147,17 @@ namespace Server.Multis return table; } - private void LoadItems(string path, params string[] itemColumns) + private void LoadItems(string path, params ReadOnlySpan itemColumns) { LoadSpreadsheet(m_ItemTable, path, itemColumns); } - private void LoadMultis(string path, params string[] multiColumns) + private void LoadMultis(string path, params ReadOnlySpan multiColumns) { LoadSpreadsheet(m_MultiTable, path, multiColumns); } - private void LoadSpreadsheet(int[] table, string path, params string[] tileColumns) + private void LoadSpreadsheet(int[] table, string path, params ReadOnlySpan tileColumns) { var ss = new Spreadsheet(path); diff --git a/Projects/UOContent/Multis/Houses/BaseHouse.cs b/Projects/UOContent/Multis/Houses/BaseHouse.cs index 095f95569..d49062d01 100644 --- a/Projects/UOContent/Multis/Houses/BaseHouse.cs +++ b/Projects/UOContent/Multis/Houses/BaseHouse.cs @@ -2334,9 +2334,7 @@ namespace Server.Multis } else { - Container c = fromState.AddTrade(toState); - - c.DropItem(new TransferItem(this)); + fromState.AddTrade(toState).DropItem(new TransferItem(this)); } } } @@ -3357,9 +3355,7 @@ namespace Server.Multis { for (var i = 0; i < Doors.Count; ++i) { - Item item = Doors[i]; - - item?.Delete(); + Doors[i]?.Delete(); } Doors.Clear(); diff --git a/Projects/UOContent/Multis/Houses/HouseFoundation.cs b/Projects/UOContent/Multis/Houses/HouseFoundation.cs index 0620f9f62..46be9fdfa 100644 --- a/Projects/UOContent/Multis/Houses/HouseFoundation.cs +++ b/Projects/UOContent/Multis/Houses/HouseFoundation.cs @@ -2139,7 +2139,7 @@ namespace Server.Multis if (_newPrice - _oldPrice < 0) { - builder. AddHtmlLocalized(10, 260, 150, 20, 1062059, 992); // Your Refund: + builder.AddHtmlLocalized(10, 260, 150, 20, 1062059, 992); // Your Refund: builder.AddLabel(170, 260, 70, (_oldPrice - _newPrice).ToString()); } else diff --git a/Projects/UOContent/Multis/Houses/MovingCrate.cs b/Projects/UOContent/Multis/Houses/MovingCrate.cs index 7388323fd..a52ad7aec 100644 --- a/Projects/UOContent/Multis/Houses/MovingCrate.cs +++ b/Projects/UOContent/Multis/Houses/MovingCrate.cs @@ -72,19 +72,18 @@ namespace Server.Multis { if (item is PackingBox packingBox) { - Container box = packingBox; - var subItems = box.Items; + var subItems = packingBox.Items; if (subItems.Count < MaxItemsPerSubcontainer) { - box.DropItem(dropped); + packingBox.DropItem(dropped); return; } } } // 3. Drop the item into a new container - Container subContainer = new PackingBox(); + var subContainer = new PackingBox(); subContainer.DropItem(dropped); var location = GetFreeLocation(); diff --git a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs index 3a658effe..1026c986a 100644 --- a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs @@ -424,7 +424,7 @@ public static class IncomingAccountPackets if (state.Seed == 0) { state.LogInfo("Invalid client detected, disconnecting"); - state.Disconnect("Duplicate seed sent."); + state.Disconnect("Invalid client detected"); return; } diff --git a/Projects/UOContent/Regions/BaseRegion.cs b/Projects/UOContent/Regions/BaseRegion.cs index dbe924594..b18c7fa18 100644 --- a/Projects/UOContent/Regions/BaseRegion.cs +++ b/Projects/UOContent/Regions/BaseRegion.cs @@ -12,7 +12,7 @@ public class BaseRegion : Region private static readonly List m_RectBuffer1 = new(); private static readonly List m_RectBuffer2 = new(); - public BaseRegion(string name, Map map, int priority, params Rectangle2D[] area) : base(name, map, priority, area) + public BaseRegion(string name, Map map, int priority, params ReadOnlySpan area) : base(name, map, priority, area) { } @@ -26,7 +26,7 @@ public class BaseRegion : Region { } - public BaseRegion(string name, Map map, Region parent, params Rectangle2D[] area) : base(name, map, parent, area) + public BaseRegion(string name, Map map, Region parent, params ReadOnlySpan area) : base(name, map, parent, area) { } diff --git a/Projects/UOContent/Regions/GuardedRegion.cs b/Projects/UOContent/Regions/GuardedRegion.cs index 28050a515..bfd54bd03 100644 --- a/Projects/UOContent/Regions/GuardedRegion.cs +++ b/Projects/UOContent/Regions/GuardedRegion.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Server.Collections; using Server.Mobiles; namespace Server.Regions; @@ -151,32 +152,15 @@ public class GuardedRegion : BaseRegion public override void MakeGuard(Mobile focus) { - BaseGuard useGuard = null; - foreach (var m in focus.GetMobilesInRange(8)) - { - if (m.Focus == null) - { - useGuard = m; - break; - } - } + m_GuardParams[0] = focus; - if (useGuard == null) + try { - m_GuardParams[0] = focus; - - try - { - GuardType.CreateInstance(m_GuardParams); - } - catch - { - // ignored - } + GuardType.CreateInstance(m_GuardParams); } - else + catch { - useGuard.Focus = focus; + // ignored } } @@ -187,7 +171,7 @@ public class GuardedRegion : BaseRegion return; } - if (!AllowReds && m.Kills >= 5) + if (!AllowReds && (m.Kills >= 5 || m is BaseCreature { AlwaysMurderer: true })) { CheckGuardCandidate(m); } @@ -251,7 +235,7 @@ public class GuardedRegion : BaseRegion public void CheckGuardCandidate(Mobile m) { - if (IsDisabled() || !IsGuardCandidate(m)) + if (!IsGuardCandidate(m)) { return; } @@ -276,16 +260,18 @@ public class GuardedRegion : BaseRegion foreach (var v in m.GetMobilesInRange(8)) { - if (!v.Player && v != m && !IsGuardCandidate(v) && - ((v as BaseCreature)?.IsHumanInTown() ?? v.Body.IsHuman && v.Region.IsPartOf(this))) + if (v.Player || v == m || IsGuardCandidate(v) || + !((v as BaseCreature)?.IsHumanInTown() ?? v.Body.IsHuman && v.Region.IsPartOf(this))) { - var dist = m.GetDistanceToSqrt(v); + continue; + } - if (fakeCall == null || dist < prio) - { - fakeCall = v; - prio = dist; - } + var dist = m.GetDistanceToSqrt(v); + + if (fakeCall == null || dist < prio) + { + fakeCall = v; + prio = dist; } } @@ -293,7 +279,7 @@ public class GuardedRegion : BaseRegion { fakeCall.Say(Utility.Random(1013037, 16)); - MakeGuard(m); + BaseGuard.Spawn(fakeCall, m); timer.Stop(); m_GuardCandidates.Remove(m); m.SendLocalizedMessage(502276); // Guards can no longer be called on you. @@ -308,32 +294,48 @@ public class GuardedRegion : BaseRegion public void CallGuards(Point3D p) { - if (IsDisabled()) - { - return; - } + using var queue = PooledRefQueue.Create(); foreach (var m in Map.GetMobilesInRange(p, 14)) { - if (IsGuardCandidate(m) && - (!AllowReds && m.Kills >= 5 && m.Region.IsPartOf(this) || m_GuardCandidates.ContainsKey(m))) + if (!IsGuardCandidate(m) || !m.Region.IsPartOf(this) && !m_GuardCandidates.ContainsKey(m)) { - if (m_GuardCandidates.Remove(m, out var timer)) - { - timer.Stop(); - } - - MakeGuard(m); - m.SendLocalizedMessage(502276); // Guards can no longer be called on you. - break; + continue; } + + if (m_GuardCandidates.Remove(m, out var timer)) + { + timer.Stop(); + } + + queue.Enqueue(m); + break; + } + + while (queue.Count > 0) + { + var m = queue.Dequeue(); + BaseGuard.Spawn(this, m); + m.SendLocalizedMessage(502276); // Guards can no longer be called on you. } } - public bool IsGuardCandidate(Mobile m) => - m is not BaseGuard && m.Alive && m.AccessLevel <= AccessLevel.Player && !m.Blessed && - (m is not BaseCreature creature || !creature.IsInvulnerable) && !IsDisabled() && - (!AllowReds && m.Kills >= 5 || m.Criminal); + public bool IsGuardCandidate(Mobile m) + { + if (m is BaseGuard || !m.Alive || m.AccessLevel > AccessLevel.Player || m.Blessed) + { + return false; + } + + var bc = m as BaseCreature; + + if (bc?.IsInvulnerable == true) + { + return false; + } + + return !AllowReds && (m.Kills >= 5 || bc?.AlwaysMurderer == true) || m.Criminal; + } private class GuardTimer : Timer { diff --git a/Projects/UOContent/Spells/Fifth/DispelField.cs b/Projects/UOContent/Spells/Fifth/DispelField.cs index 9e805bf10..909ea4377 100644 --- a/Projects/UOContent/Spells/Fifth/DispelField.cs +++ b/Projects/UOContent/Spells/Fifth/DispelField.cs @@ -22,6 +22,8 @@ namespace Server.Spells.Fifth public override SpellCircle Circle => SpellCircle.Fifth; + public int TargetRange => Core.T2A ? 15 : 18; + public void Target(Item item) { if (!item.GetType().IsDefined(typeof(DispellableFieldAttribute), false)) diff --git a/Projects/UOContent/Spells/Fifth/PoisonField.cs b/Projects/UOContent/Spells/Fifth/PoisonField.cs index 7f38c677f..3c5d2f609 100644 --- a/Projects/UOContent/Spells/Fifth/PoisonField.cs +++ b/Projects/UOContent/Spells/Fifth/PoisonField.cs @@ -26,6 +26,8 @@ public class PoisonFieldSpell : MagerySpell, ITargetingSpell public override SpellCircle Circle => SpellCircle.Fifth; + public int TargetRange => Core.T2A ? 15 : 18; + public void Target(IPoint3D p) { if (SpellHelper.CheckTown(p, Caster) && CheckSequence()) diff --git a/Projects/UOContent/Spells/Fourth/FireField.cs b/Projects/UOContent/Spells/Fourth/FireField.cs index 004a43b57..0b0a22129 100644 --- a/Projects/UOContent/Spells/Fourth/FireField.cs +++ b/Projects/UOContent/Spells/Fourth/FireField.cs @@ -26,6 +26,8 @@ public class FireFieldSpell : MagerySpell, ITargetingSpell public override SpellCircle Circle => SpellCircle.Fourth; + public int TargetRange => Core.T2A ? 15 : 18; + public void Target(IPoint3D p) { if (SpellHelper.CheckTown(p, Caster) && CheckSequence()) diff --git a/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs b/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs index 8ebc42ab8..b2359dbf4 100644 --- a/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs +++ b/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using ModernUO.CodeGeneratedEvents; -using Server.Engines.Quests; using Server.Engines.Quests.Necro; using Server.Items; using Server.Mobiles; @@ -115,13 +114,9 @@ public class AnimateDeadSpell : NecromancerSpell, ITargetingSpell if (comp?.Addon is MaabusCoffin addon) { - var pm = Caster as PlayerMobile; - - var qs = pm?.Quest; - - if (qs is DarkTidesQuest) + if (Caster is PlayerMobile { Quest : DarkTidesQuest quest }) { - QuestObjective objective = qs.FindObjective(); + var objective = quest.FindObjective(); if (objective?.Completed == false) { diff --git a/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs b/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs index abe0847d9..d65d3b135 100644 --- a/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs +++ b/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs @@ -422,7 +422,7 @@ public class AnimalForm : NinjaSpell int y = Math.DivRem(pos, 2, out var rem) * 64 + 44; int x = rem == 0 ? 14 : 264; - Rectangle2D b = ItemBounds.Table[entry.ItemID]; + Rectangle2D b = ItemBounds.Bounds[entry.ItemID]; builder.AddImageTiledButton(x, y, 0x918, 0x919, i + 1, GumpButtonType.Reply, 0, entry.ItemID, entry.Hue, 40 - b.Width / 2 - b.X, 30 - b.Height / 2 - b.Y, entry.Tooltip); diff --git a/Projects/UOContent/Spells/Seventh/EnergyField.cs b/Projects/UOContent/Spells/Seventh/EnergyField.cs index 87dedeaf7..a6fc1b116 100644 --- a/Projects/UOContent/Spells/Seventh/EnergyField.cs +++ b/Projects/UOContent/Spells/Seventh/EnergyField.cs @@ -26,6 +26,8 @@ public class EnergyFieldSpell : MagerySpell, ITargetingSpell public override SpellCircle Circle => SpellCircle.Seventh; + public int TargetRange => Core.T2A ? 15 : 18; + public void Target(IPoint3D p) { if (SpellHelper.CheckTown(p, Caster) && CheckSequence()) @@ -55,8 +57,7 @@ public class EnergyFieldSpell : MagerySpell, ITargetingSpell continue; } - Item item = new EnergyField(targetLoc, Caster.Map, duration, itemID, Caster); - item.ProcessDelta(); + new EnergyField(targetLoc, Caster.Map, duration, itemID, Caster).ProcessDelta(); Effects.SendLocationParticles( EffectItem.Create(targetLoc, Caster.Map, EffectItem.DefaultDuration), diff --git a/Projects/UOContent/Spells/Sixth/ParalyzeField.cs b/Projects/UOContent/Spells/Sixth/ParalyzeField.cs index ce2ec48f0..830f16dfe 100644 --- a/Projects/UOContent/Spells/Sixth/ParalyzeField.cs +++ b/Projects/UOContent/Spells/Sixth/ParalyzeField.cs @@ -25,6 +25,8 @@ public class ParalyzeFieldSpell : MagerySpell, ITargetingSpell public override SpellCircle Circle => SpellCircle.Sixth; + public int TargetRange => Core.T2A ? 15 : 18; + public void Target(IPoint3D p) { if (SpellHelper.CheckTown(p, Caster) && CheckSequence()) @@ -55,8 +57,7 @@ public class ParalyzeFieldSpell : MagerySpell, ITargetingSpell continue; } - Item item = new ParalyzeField(Caster, itemID, targetLoc, Caster.Map, duration); - item.ProcessDelta(); + new ParalyzeField(Caster, itemID, targetLoc, Caster.Map, duration).ProcessDelta(); Effects.SendLocationParticles( EffectItem.Create(targetLoc, Caster.Map, EffectItem.DefaultDuration), diff --git a/Projects/UOContent/Spells/Targeting/ITargetingSpell.cs b/Projects/UOContent/Spells/Targeting/ITargetingSpell.cs index f2671e1a5..3c23dcbd8 100644 --- a/Projects/UOContent/Spells/Targeting/ITargetingSpell.cs +++ b/Projects/UOContent/Spells/Targeting/ITargetingSpell.cs @@ -4,7 +4,8 @@ public interface IRangedSpell { Mobile Caster { get; } - int TargetRange => Core.ML ? 10 : 12; + // https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/1999-2/1999-06-14th-april/ + int TargetRange => Core.T2A ? 10 : 12; void FinishSequence(); } diff --git a/Projects/UOContent/Spells/Third/WallOfStone.cs b/Projects/UOContent/Spells/Third/WallOfStone.cs index f1c94c6fd..7cae3b848 100644 --- a/Projects/UOContent/Spells/Third/WallOfStone.cs +++ b/Projects/UOContent/Spells/Third/WallOfStone.cs @@ -23,6 +23,8 @@ public class WallOfStoneSpell : MagerySpell, ITargetingSpell public override SpellCircle Circle => SpellCircle.Third; + public int TargetRange => Core.T2A ? 15 : 18; + public void Target(IPoint3D p) { if (SpellHelper.CheckTown(p, Caster) && CheckSequence()) diff --git a/Projects/UOContent/Targets/BladedItemTarget.cs b/Projects/UOContent/Targets/BladedItemTarget.cs index acb2e0101..c1d7b4c87 100644 --- a/Projects/UOContent/Targets/BladedItemTarget.cs +++ b/Projects/UOContent/Targets/BladedItemTarget.cs @@ -71,47 +71,43 @@ namespace Server.Targets } } - HarvestSystem system = Lumberjacking.System; + var system = Lumberjacking.System; var def = system.GetDefinition(); - if (!system.GetHarvestDetails(from, m_Item, targeted, out var tileID, out var map, out var loc, out var isLand)) + if (!system.GetHarvestDetails(from, m_Item, targeted, out var tileID, out var map, out var loc, out var isLand) + || !def.Validate(tileID, isLand)) { from.SendLocalizedMessage(500494); // You can't use a bladed item on that! + return; } - else if (!def.Validate(tileID, isLand)) + + var bank = def.GetBank(map, loc.X, loc.Y); + + if (bank == null) { - from.SendLocalizedMessage(500494); // You can't use a bladed item on that! + return; + } + + if (bank.Current < 5) + { + from.SendLocalizedMessage(500493); // There's not enough wood here to harvest. } else { - var bank = def.GetBank(map, loc.X, loc.Y); + bank.Consume(5, from); - if (bank == null) - { - return; - } + var item = new Kindling(); - if (bank.Current < 5) + if (from.PlaceInBackpack(item)) { - from.SendLocalizedMessage(500493); // There's not enough wood here to harvest. + from.SendLocalizedMessage(500491); // You put some kindling into your backpack. + from.SendLocalizedMessage(500492); // An axe would probably get you more wood. } else { - bank.Consume(5, from); + from.SendLocalizedMessage(500490); // You can't place any kindling into your backpack! - Item item = new Kindling(); - - if (from.PlaceInBackpack(item)) - { - from.SendLocalizedMessage(500491); // You put some kindling into your backpack. - from.SendLocalizedMessage(500492); // An axe would probably get you more wood. - } - else - { - from.SendLocalizedMessage(500490); // You can't place any kindling into your backpack! - - item.Delete(); - } + item.Delete(); } } } diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index 13beab686..a1146db1c 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -39,8 +39,8 @@ false - - + + @@ -48,7 +48,7 @@ - + diff --git a/Projects/UOContent/World Saves/AutoArchive.cs b/Projects/UOContent/World Saves/AutoArchive.cs index 43579cfe9..1c25d34cf 100755 --- a/Projects/UOContent/World Saves/AutoArchive.cs +++ b/Projects/UOContent/World Saves/AutoArchive.cs @@ -383,7 +383,7 @@ namespace Server.Saves _ => date }; - private static IEnumerable PathsByTimestampName(string path, bool files = false) + private static SortedDictionary.ValueCollection PathsByTimestampName(string path, bool files = false) { var allItems = files ? Directory.EnumerateFiles(path) : Directory.GetDirectories(path); var items = new SortedDictionary(new DescendingComparer()); diff --git a/docs/commands/commands.7z b/docs/commands/commands.7z index d0d6c0e9b..6597b7832 100644 Binary files a/docs/commands/commands.7z and b/docs/commands/commands.7z differ