From 40479c946af878243349a8da388aa8e5d655b459 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Mon, 10 Feb 2025 19:31:39 -0800 Subject: [PATCH 01/53] feat: Adds item graphic size to Bounds.bin and makes item graphic offset available to gumps (#2115) --- Distribution/Data/Binary/Bounds.bin | Bin 524288 -> 0 bytes Distribution/Data/Items/ItemBounds.bin | Bin 0 -> 786432 bytes Projects/Server/Client/ArtData.cs | 12 +-- Projects/Server/Items/Item.cs | 2 +- Projects/Server/Items/ItemBounds.cs | 89 +++++++++++------- .../Object Creation/CategorizedAddGump.cs | 2 +- .../Engines/Factions/Gumps/SheriffGump.cs | 2 +- Projects/UOContent/Gumps/Base/BaseGump.cs | 29 +++++- Projects/UOContent/Gumps/RewardGump.cs | 2 +- .../Mutation Core/PlagueBeastBackpack.cs | 4 +- Projects/UOContent/Mobiles/PlayerMobile.cs | 2 +- .../UOContent/Spells/Ninjitsu/AnimalForm.cs | 2 +- 12 files changed, 96 insertions(+), 50 deletions(-) delete mode 100644 Distribution/Data/Binary/Bounds.bin create mode 100644 Distribution/Data/Items/ItemBounds.bin diff --git a/Distribution/Data/Binary/Bounds.bin b/Distribution/Data/Binary/Bounds.bin deleted file mode 100644 index 11431362eb0367d45527e9d2b5ef3be3ece5e53f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524288 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́qW(@RzFAL1-q^pPDZkrTnGo~vf7a%w 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 z4wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA z=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^ zU=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$ z1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3 zIbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0e zm;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVx zfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e1 z4wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA z=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^ zU=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$ z1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3 zIbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0e zm;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVx zfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e1 z4wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA z=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^ zU=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$ z1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3 zIbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0e zm;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVx zfH`0em;>g3IbaT$1LlA^U=ElA=72e14wwVxfH`0em;>g3IbaT$1LlA^U=ElA=72e1 L4wwVKsvP+LC0T+s literal 0 HcmV?d00001 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/Items/Item.cs b/Projects/Server/Items/Item.cs index 2a17627ff..0fd0dc0bd 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -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) { 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/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/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/Gumps/Base/BaseGump.cs b/Projects/UOContent/Gumps/Base/BaseGump.cs index a3b393b7a..2682ce872 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,31 @@ 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); + } } 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/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/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index 9da05f1c1..54c9fe4b0 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -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/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); From 2d0957119e272dc0808992891997f1cc5526cdd9 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Mon, 10 Feb 2025 19:53:47 -0800 Subject: [PATCH 02/53] feat: Adds BaseGump.GetGumpOffsetForItemGraphic (#2116) --- Projects/UOContent/Gumps/Base/BaseGump.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Projects/UOContent/Gumps/Base/BaseGump.cs b/Projects/UOContent/Gumps/Base/BaseGump.cs index 2682ce872..439146a3c 100644 --- a/Projects/UOContent/Gumps/Base/BaseGump.cs +++ b/Projects/UOContent/Gumps/Base/BaseGump.cs @@ -113,4 +113,10 @@ public abstract class BaseGump 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); + } } From af027cdac6a22e195ae4c36c87dc4997ea67f237 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 12 Feb 2025 20:50:28 -0800 Subject: [PATCH 03/53] Bumps dependencies. Fixes bugs with serialization generator (#2119) --- .config/dotnet-tools.json | 2 +- Projects/Server.Tests/Server.Tests.csproj | 4 ++-- Projects/Server/Server.csproj | 4 ++-- Projects/UOContent.Tests/UOContent.Tests.csproj | 4 ++-- Projects/UOContent/UOContent.csproj | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 73eb8d9a0..55fb6b119 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.19", "commands": [ "ModernUOSchemaGenerator" ] 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/Server.csproj b/Projects/Server/Server.csproj index 9689bfd32..8309adcea 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -37,10 +37,10 @@ - + - + diff --git a/Projects/UOContent.Tests/UOContent.Tests.csproj b/Projects/UOContent.Tests/UOContent.Tests.csproj index a6c296cef..daefd857e 100644 --- a/Projects/UOContent.Tests/UOContent.Tests.csproj +++ b/Projects/UOContent.Tests/UOContent.Tests.csproj @@ -4,9 +4,9 @@ Debug;Release;Analyze - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index 13beab686..26b7ad3a2 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -39,8 +39,8 @@ false - - + + @@ -48,7 +48,7 @@ - + From 41e909cd224359b6a82f2dcf1aa3cf176815767d Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 12 Feb 2025 22:30:46 -0800 Subject: [PATCH 04/53] fix: Fixes serialization generator issue with arrays (#2120) --- .config/dotnet-tools.json | 2 +- Projects/Server/Server.csproj | 2 +- Projects/UOContent/UOContent.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 55fb6b119..08b49e256 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "modernuoschemagenerator": { - "version": "2.12.19", + "version": "2.12.20", "commands": [ "ModernUOSchemaGenerator" ] diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index 8309adcea..bfc16b87b 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -40,7 +40,7 @@ - + diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index 26b7ad3a2..535dfcd13 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -48,7 +48,7 @@ - + From 3cf1bd0f7d128c395bb7642c6a32339b27d05388 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 13 Feb 2025 18:53:24 -0800 Subject: [PATCH 05/53] fix: Fixes AddLabelPlaceholder on StaticGump (#2121) --- Projects/UOContent/Engines/Help/PagePromptGump.cs | 2 +- Projects/UOContent/Gumps/Base/StaticGumpBuilder.cs | 2 +- Projects/UOContent/Multis/Houses/HouseFoundation.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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/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 From 899146dcb7c28b873f770aa8b4dc84c0608ebc63 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:12:28 -0800 Subject: [PATCH 06/53] fix: Fixes static gumps sending too much data. (#2122) --- Projects/UOContent/Gumps/Base/StaticGump.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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); } } From 6d7b1b8bee4d92d85223cdd57b6d9d1ee328de84 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:30:47 -0800 Subject: [PATCH 07/53] fix: Adds missing string interpolation handler for SetStringSlot (#2123) --- .../Tests/Gumps/TestGumps/StaticLayoutTestGump.cs | 5 ++++- Projects/UOContent/Engines/Khaldun/PuzzleChest.cs | 4 ++-- Projects/UOContent/Gumps/Base/GumpStringsBuilder.cs | 8 ++++++++ Projects/UOContent/Gumps/PetResurrectGump.cs | 4 +++- Projects/UOContent/Gumps/StaticWarningGump.cs | 3 ++- Projects/UOContent/Gumps/TithingGump.cs | 4 ++-- 6 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs b/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs index 06679a48e..08d561c09 100644 --- a/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs +++ b/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs @@ -1,3 +1,5 @@ +using System; +using Server.Buffers; using Server.Gumps; namespace Server.Tests.Gumps; @@ -35,6 +37,7 @@ public class StaticLayoutTestGump : StaticGump protected override void BuildStrings(ref GumpStringsBuilder builder) { - builder.SetStringSlot("petName", $"
    {_petName}
    "); + var petNameText = _petName.AsSpan().Center(); + builder.SetStringSlot("petName", ref petNameText); } } 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/Gumps/Base/GumpStringsBuilder.cs b/Projects/UOContent/Gumps/Base/GumpStringsBuilder.cs index 798dc6a51..4a8bc3147 100644 --- a/Projects/UOContent/Gumps/Base/GumpStringsBuilder.cs +++ b/Projects/UOContent/Gumps/Base/GumpStringsBuilder.cs @@ -71,6 +71,14 @@ public ref struct GumpStringsBuilder handler.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetStringSlot(ref RawInterpolatedStringHandler slotKeyHandler, ref RawInterpolatedStringHandler handler) + { + SetStringSlot(slotKeyHandler.Text, handler.Text); + slotKeyHandler.Clear(); + handler.Clear(); + } + public void SetStringSlot(ReadOnlySpan slotKey, ReadOnlySpan text) { var hash = HashUtility.ComputeHash64(slotKey); diff --git a/Projects/UOContent/Gumps/PetResurrectGump.cs b/Projects/UOContent/Gumps/PetResurrectGump.cs index c2c28ded4..0dff0c08f 100644 --- a/Projects/UOContent/Gumps/PetResurrectGump.cs +++ b/Projects/UOContent/Gumps/PetResurrectGump.cs @@ -1,3 +1,4 @@ +using System; using Server.Mobiles; using Server.Network; @@ -39,7 +40,8 @@ public class PetResurrectGump : StaticGump protected override void BuildStrings(ref GumpStringsBuilder builder) { - builder.SetStringSlot("petName", $"
    {_pet.Name}
    "); + var petNameText = _pet.Name.AsSpan().Center(); + builder.SetStringSlot("petName", ref petNameText); } public override void OnResponse(NetState state, in RelayInfo info) diff --git a/Projects/UOContent/Gumps/StaticWarningGump.cs b/Projects/UOContent/Gumps/StaticWarningGump.cs index ef98d4adc..f90f1d35b 100644 --- a/Projects/UOContent/Gumps/StaticWarningGump.cs +++ b/Projects/UOContent/Gumps/StaticWarningGump.cs @@ -88,7 +88,8 @@ public abstract class StaticWarningGump : StaticGump where T : StaticWarni protected sealed override void BuildStrings(ref GumpStringsBuilder builder) { - builder.SetStringSlot("content", Content.Color(ContentColor)); + var contentText = Content.AsSpan().Color(ContentColor); + builder.SetStringSlot("content", ref contentText); } 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) From 90059c5e7464daf7950084e1bb46836486f126d0 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:58:46 -0800 Subject: [PATCH 08/53] feat: Adds convenience methods to GumpStringsBuilder (#2124) --- Projects/Server/Utilities/Html.cs | 210 +++++++++++++++--- .../Gumps/TestGumps/StaticLayoutTestGump.cs | 3 +- .../Gumps/Base/GumpStringsBuilder.cs | 86 ++++++- Projects/UOContent/Gumps/PetResurrectGump.cs | 3 +- Projects/UOContent/Gumps/StaticWarningGump.cs | 3 +- 5 files changed, 269 insertions(+), 36 deletions(-) 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/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs b/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs index 08d561c09..479f79c8d 100644 --- a/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs +++ b/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs @@ -37,7 +37,6 @@ public class StaticLayoutTestGump : StaticGump protected override void BuildStrings(ref GumpStringsBuilder builder) { - var petNameText = _petName.AsSpan().Center(); - builder.SetStringSlot("petName", ref petNameText); + builder.SetHtmlTextCentered("petName", _petName); } } diff --git a/Projects/UOContent/Gumps/Base/GumpStringsBuilder.cs b/Projects/UOContent/Gumps/Base/GumpStringsBuilder.cs index 4a8bc3147..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) { @@ -74,11 +150,17 @@ public ref struct GumpStringsBuilder [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetStringSlot(ref RawInterpolatedStringHandler slotKeyHandler, ref RawInterpolatedStringHandler handler) { - SetStringSlot(slotKeyHandler.Text, handler.Text); - slotKeyHandler.Clear(); + 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/PetResurrectGump.cs b/Projects/UOContent/Gumps/PetResurrectGump.cs index 0dff0c08f..4ccb84518 100644 --- a/Projects/UOContent/Gumps/PetResurrectGump.cs +++ b/Projects/UOContent/Gumps/PetResurrectGump.cs @@ -40,8 +40,7 @@ public class PetResurrectGump : StaticGump protected override void BuildStrings(ref GumpStringsBuilder builder) { - var petNameText = _pet.Name.AsSpan().Center(); - builder.SetStringSlot("petName", ref petNameText); + builder.SetHtmlTextCentered("petName", _pet.Name); } public override void OnResponse(NetState state, in RelayInfo info) diff --git a/Projects/UOContent/Gumps/StaticWarningGump.cs b/Projects/UOContent/Gumps/StaticWarningGump.cs index f90f1d35b..1129fb36b 100644 --- a/Projects/UOContent/Gumps/StaticWarningGump.cs +++ b/Projects/UOContent/Gumps/StaticWarningGump.cs @@ -88,8 +88,7 @@ public abstract class StaticWarningGump : StaticGump where T : StaticWarni protected sealed override void BuildStrings(ref GumpStringsBuilder builder) { - var contentText = Content.AsSpan().Color(ContentColor); - builder.SetStringSlot("content", ref contentText); + builder.SetHtmlText("content", Content, ContentColor); } public override void OnResponse(NetState sender, in RelayInfo info) => _callback?.Invoke(info.ButtonID == 1); From 279b10dd0f9b9148601c40b559cd238015fa4c54 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 13 Feb 2025 21:19:02 -0800 Subject: [PATCH 09/53] feat: Replaces params array with params ReadOnlySpan (#2125) --- Projects/Server/Items/Container.cs | 2 +- Projects/Server/Regions/Region.cs | 6 +++--- Projects/Server/Utilities/Utility.cs | 8 ++++---- Projects/UOContent/Engines/Spawners/BaseSpawner.cs | 4 ++-- Projects/UOContent/Engines/Spawners/ProximitySpawner.cs | 4 ++-- Projects/UOContent/Engines/Spawners/RegionSpawner.cs | 4 ++-- Projects/UOContent/Engines/Spawners/Spawner.cs | 4 ++-- Projects/UOContent/Gumps/AdminGump.cs | 2 +- Projects/UOContent/Items/Misc/FlippableAddonAttribute.cs | 4 ++-- Projects/UOContent/Misc/InhumanSpeech.cs | 4 ++-- Projects/UOContent/Misc/Loot.cs | 8 ++++---- Projects/UOContent/Misc/Weather.cs | 8 ++++---- Projects/UOContent/Mobiles/AI/HealerAI.cs | 3 ++- Projects/UOContent/Multis/ComponentVerification.cs | 6 +++--- Projects/UOContent/Regions/BaseRegion.cs | 4 ++-- 15 files changed, 36 insertions(+), 35 deletions(-) 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/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/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 61545077d..9f61d7446 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) 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/Gumps/AdminGump.cs b/Projects/UOContent/Gumps/AdminGump.cs index 816d1967b..1d8a8a024 100644 --- a/Projects/UOContent/Gumps/AdminGump.cs +++ b/Projects/UOContent/Gumps/AdminGump.cs @@ -1313,7 +1313,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; 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/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/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/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/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/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) { } From 2e08eda8cbffb4c32e194f7aa82f3a2fd2df3e5f Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:15:14 -0800 Subject: [PATCH 10/53] feat: Standardizes South/East selection gumps (#1839) --- .../UOContent/Engines/BuffIcons/BuffInfo.cs | 1 - Projects/UOContent/Gumps/PetResurrectGump.cs | 1 - .../Gumps/SelectAddonDirectionGump.cs | 49 +++++++++++++++ .../Holiday Stuff/Christmas/2004/Mistletoe.cs | 46 +++++++------- .../Christmas/2010/Addons/FireFliesDeed.cs | 37 +++++------ .../Items/Deeds/IDirectionAddonDeed.cs | 8 +++ .../Broken Furniture Collection/BrokenBed.cs | 47 +++----------- .../BrokenVanity.cs | 48 +++----------- .../Items/Special/Gifts/HearthOfHomeFire.cs | 22 ++++--- .../Items/Special/Heritage Items/Curtains.cs | 47 +++----------- .../Special/Heritage Items/HangingAxes.cs | 47 +++----------- .../Special/Heritage Items/HangingSwords.cs | 47 +++----------- .../Special/Heritage Items/HouseLadder.cs | 59 ++++++++--------- .../Items/Special/Heritage Items/Statue.cs | 47 +++----------- .../Items/Special/Heritage Items/UnmadeBed.cs | 48 +++----------- .../Items/Special/Heritage Items/Vanity.cs | 47 +++----------- .../Special/Heritage Items/WoodenCoffin.cs | 47 +++----------- .../UOContent/Items/Special/Holiday/Wreath.cs | 44 +++++++------ .../Items/Special/Veteran Rewards/Banner.cs | 63 ++++++++----------- .../Veteran Rewards/HangingSkeleton.cs | 27 ++++---- .../Monsters/Summons/SummonedAirElemental.cs | 1 - 21 files changed, 263 insertions(+), 520 deletions(-) create mode 100644 Projects/UOContent/Gumps/SelectAddonDirectionGump.cs create mode 100644 Projects/UOContent/Items/Deeds/IDirectionAddonDeed.cs 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/Gumps/PetResurrectGump.cs b/Projects/UOContent/Gumps/PetResurrectGump.cs index 4ccb84518..103ed8255 100644 --- a/Projects/UOContent/Gumps/PetResurrectGump.cs +++ b/Projects/UOContent/Gumps/PetResurrectGump.cs @@ -1,4 +1,3 @@ -using System; using Server.Mobiles; using Server.Network; 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/Holiday Stuff/Christmas/2004/Mistletoe.cs b/Projects/UOContent/Holiday Stuff/Christmas/2004/Mistletoe.cs index f52e49dd7..0fd6f4217 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 { @@ -276,47 +276,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/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/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..a715fd1dc 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 { @@ -267,47 +267,45 @@ public partial class WreathDeed : Item } } - 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/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/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 From 525112943cf209971c764935fa39751f518ab161 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:18:38 -0800 Subject: [PATCH 11/53] fix: Changes Chyloth gump to static (#2126) --- .../Quests/The Summoning/Mobiles/Chyloth.cs | 93 ++++++++++--------- .../Gumps/CharacterStatueGump.cs | 4 +- 2 files changed, 52 insertions(+), 45 deletions(-) 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/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 { From d10b32aed3a8d483ae51dd9b20d8b560bc39cc07 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:23:49 -0800 Subject: [PATCH 12/53] fix: Fixes dead plant serialization (#2128) --- Projects/UOContent/Engines/Plants/PlantItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From 9b361822f36cf246e3ed0e122afeae655a91744f Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:36:29 -0800 Subject: [PATCH 13/53] fix: Fixes issue with delete all of missing type on deserialize (#2129) --- .../Serialization/GenericEntityPersistence.cs | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) 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(); From 0265e0673af33d18d32c2912559955f050bd8666 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 25 Feb 2025 20:46:20 -0800 Subject: [PATCH 14/53] fix: Fixes empty spellbooks with EA client on Pre-AOS (#2131) --- Projects/Server/Network/Packets/OutgoingContainerPackets.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From b8a8bfd1a9a33c7e580fd5feeadab4273bca81dd Mon Sep 17 00:00:00 2001 From: Voxpire Date: Thu, 27 Feb 2025 02:43:20 +0000 Subject: [PATCH 15/53] fix: Fixes console clobbering when prompted to add an owner account (#2133) ### Summary - Updates AccountPrompt to use `logger` to avoid console clobbering ### Screenshot ![image](https://github.com/user-attachments/assets/8bfbaccc-3c23-4d05-ab49-a656e6a8803f) --- Projects/UOContent/Misc/AccountPrompt.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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) From bcab91255a4cba2dd5c9b05487800652888653a4 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 26 Feb 2025 18:43:49 -0800 Subject: [PATCH 16/53] fix: Fixes EVs/BSs attacking players in Pre-AOS (#2132) --- Projects/UOContent/Misc/Notoriety.cs | 8 +++++- Projects/UOContent/Mobiles/AI/BaseAI.cs | 26 ++++++------------- Projects/UOContent/Mobiles/BaseCreature.cs | 2 ++ .../Monsters/Misc/Melee/BladeSpirits.cs | 4 ++- .../Monsters/Misc/Melee/EnergyVortex.cs | 2 ++ 5 files changed, 22 insertions(+), 20 deletions(-) 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/Mobiles/AI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI.cs index 5ce7ba5a7..555c65a46 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI.cs @@ -2672,39 +2672,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 +2759,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/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index dc7e91ceb..6dde04f62 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; 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); From 2a8c62e8be1b1757765953c3889930f239032d1d Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 27 Feb 2025 22:19:38 -0800 Subject: [PATCH 17/53] fix: Fixes TCPServer accept async, makes Firewall/IP Limiter multithreaded (#2134) --- .../Tests/Network/Firewall/FirewallTests.cs | 137 ++++++++++++ Projects/Server/Client/ClientVersion.cs | 1 + Projects/Server/Client/UOClient.cs | 2 + Projects/Server/EventLoopTasks.cs | 35 +++- Projects/Server/Network/Firewall/Firewall.cs | 117 ++++++++--- .../Server/Network/Firewall/IFirewallEntry.cs | 4 +- Projects/Server/Network/IPLimiter.cs | 108 ---------- Projects/Server/Network/IPRateLimiter.cs | 197 ++++++++++++++++++ Projects/Server/Network/TcpServer.cs | 112 ++++++---- Projects/Server/Server.csproj | 3 + .../Gumps/TestGumps/StaticLayoutTestGump.cs | 2 - Projects/UOContent/Gumps/AdminGump.cs | 32 ++- Projects/UOContent/Misc/AdminFirewall.cs | 11 +- Projects/UOContent/Misc/ClientVerification.cs | 30 ++- .../Network/Packets/IncomingAccountPackets.cs | 2 +- 15 files changed, 578 insertions(+), 215 deletions(-) create mode 100644 Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs delete mode 100644 Projects/Server/Network/IPLimiter.cs create mode 100644 Projects/Server/Network/IPRateLimiter.cs 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/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 _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/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/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index d2c6751c8..eaf39af37 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; 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,92 @@ 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 + } + } + } + + private static async ValueTask ProcessSocketConnection(Socket socket) + { + byte[] firstBytes = ArrayPool.Shared.Rent(83); + + 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 = + // Older clients only send the 4 byte seed first + (UOClient.MinRequired == null || UOClient.MinRequired < ClientVersion.Version6050) && bytesRead == 4 || + (UOClient.MaxRequired == null || UOClient.MaxRequired >= ClientVersion.Version6050) && ( + // Account Login - 0xEF + 0x80 (83 bytes) + bytesRead >= 83 && firstBytes[0] == 0xEF && firstBytes[21] == 0x80 || + // Game Login - 4 bytes + 0x91 (69 bytes) + firstBytes[4] == 0x91 && bytesRead >= 69 + ); + + // 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); + } + finally + { + ArrayPool.Shared.Return(firstBytes); + cts.Dispose(); } } @@ -182,22 +230,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/Server.csproj b/Projects/Server/Server.csproj index bfc16b87b..2fb3d72ef 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -44,5 +44,8 @@
    + + <_Parameter1>Server.Tests + diff --git a/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs b/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs index 479f79c8d..85f5ec732 100644 --- a/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs +++ b/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs @@ -1,5 +1,3 @@ -using System; -using Server.Buffers; using Server.Gumps; namespace Server.Tests.Gumps; diff --git a/Projects/UOContent/Gumps/AdminGump.cs b/Projects/UOContent/Gumps/AdminGump.cs index 1d8a8a024..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)); } @@ -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/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/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; } From 49fd5f210d539c637292d0ddc388a121422d6f1a Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:50:18 -0800 Subject: [PATCH 18/53] fix: Fixes removing virtue when player is deleted (#2135) --- Projects/UOContent/Engines/Virtues/VirtueSystem.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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); } From ad361001c57e7315a87f75031fc4e7b3045e5e8d Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:00:20 -0800 Subject: [PATCH 19/53] fix: Fixes CUO connecting with new TCP Peek Filtering (#2137) --- Projects/Server/Network/TcpServer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index eaf39af37..82bbb2ddd 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -169,13 +169,15 @@ public static class TcpServer var bytesRead = await socket.ReceiveAsync(firstBytes, SocketFlags.Peek, cts.Token); var isValid = - // Older clients only send the 4 byte seed first - (UOClient.MinRequired == null || UOClient.MinRequired < ClientVersion.Version6050) && bytesRead == 4 || + // Older clients only send the 4 byte seed first then 0x80 + (UOClient.MinRequired == null || UOClient.MinRequired < ClientVersion.Version6050) && ( + bytesRead == 4 || 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 || // Game Login - 4 bytes + 0x91 (69 bytes) - firstBytes[4] == 0x91 && bytesRead >= 69 + bytesRead >= 69 && firstBytes[4] == 0x91 ); // TODO: Validate client version is v4 -> v7 for 0xEF packet From 56e3086ed174a08e8f8661f6b1df106ff8dbe1da Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 4 Mar 2025 15:55:05 -0800 Subject: [PATCH 20/53] fix: Makes hairdye gump static (#2138) --- Projects/UOContent/Items/Misc/HairDye.cs | 141 ++++++++---------- .../UOContent/Items/Misc/SpecialBeardDye.cs | 130 ++++++++-------- 2 files changed, 127 insertions(+), 144 deletions(-) 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/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); + } } } From 0024d53739e18cb5f70f21617346c84bf4dc1030 Mon Sep 17 00:00:00 2001 From: mark1145 Date: Thu, 6 Mar 2025 12:59:07 +1100 Subject: [PATCH 21/53] fix: Fixes edge cases where a leader resigns and becomes promoted in their new guild. (#2136) --- Projects/UOContent/Misc/Guild.cs | 165 ++++++++++++++++--------------- 1 file changed, 88 insertions(+), 77 deletions(-) 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: From 47da961a376eea57b21921dfe47b3076d76c039e Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:08:14 -0700 Subject: [PATCH 22/53] fix: Fixes spawning guards. Adds back non-lethal with config (#2140) --- .../UOContent/Mobiles/Guards/ArcherGuard.cs | 278 +++--------------- .../UOContent/Mobiles/Guards/BaseGuard.cs | 247 ++++++++++++++-- .../UOContent/Mobiles/Guards/WarriorGuard.cs | 247 ++-------------- 3 files changed, 298 insertions(+), 474 deletions(-) diff --git a/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs b/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs index e1719b94b..91797cc47 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()) @@ -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.DoOnTick(); } 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..9d84e8caa 100644 --- a/Projects/UOContent/Mobiles/Guards/BaseGuard.cs +++ b/Projects/UOContent/Mobiles/Guards/BaseGuard.cs @@ -1,3 +1,4 @@ +using System; using ModernUO.Serialization; using Server.Items; @@ -6,25 +7,12 @@ namespace Server.Mobiles; [SerializationGenerator(0)] public abstract partial class BaseGuard : Mobile { - public BaseGuard(Mobile target) + public static bool GuardsInstantKill { get; private set; } + public static void Configure() { - if (target != null) - { - Location = target.Location; - Map = target.Map; - - Effects.SendLocationParticles( - EffectItem.Create(Location, Map, EffectItem.DefaultDuration), - 0x3728, - 10, - 10, - 5023 - ); - } + GuardsInstantKill = ServerConfiguration.GetSetting("guards.instantKill", true); } - public abstract Mobile Focus { get; set; } - public static void Spawn(Mobile caller, Mobile target, int amount = 1, bool onlyAdditional = false) { if (target?.Deleted != false) @@ -55,6 +43,85 @@ public abstract partial class BaseGuard : Mobile } } + 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; + Map = target.Map; + + Effects.SendLocationParticles( + EffectItem.Create(Location, Map, EffectItem.DefaultDuration), + 0x3728, + 10, + 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 override void OnAfterDelete() + { + AttackTimer = null; + IdleTimer = null; + base.OnAfterDelete(); + } + public override bool OnBeforeDeath() { Effects.SendLocationParticles( @@ -66,9 +133,153 @@ 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); + } + } + } +} + +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 (_owner.Weapon is Fists) + { + _owner.Kill(); + 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..3ee7bf47f 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.DoOnTick(); } 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!"); } } } From 2c6f1854fa1a3bcc06c8b237c1dc919046bf9509 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:45:39 -0700 Subject: [PATCH 23/53] fix: Fixes guards attacking mobs (#2145) ### Summary - Fixes infinite teleport from guard refactor for spawners - Fixes guards not attacking Always Murderer creatures - Fixes crash when calling guards due to map modification while enumeration --- .../UOContent/Mobiles/Guards/BaseGuard.cs | 2 + Projects/UOContent/Regions/GuardedRegion.cs | 75 ++++++++++++------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/Projects/UOContent/Mobiles/Guards/BaseGuard.cs b/Projects/UOContent/Mobiles/Guards/BaseGuard.cs index 9d84e8caa..571b16321 100644 --- a/Projects/UOContent/Mobiles/Guards/BaseGuard.cs +++ b/Projects/UOContent/Mobiles/Guards/BaseGuard.cs @@ -203,6 +203,8 @@ public class GuardIdleTimer : Timer { BaseGuard.TeleportTo(_owner, _owner.Spawner.HomeLocation); } + + Stop(); } } } diff --git a/Projects/UOContent/Regions/GuardedRegion.cs b/Projects/UOContent/Regions/GuardedRegion.cs index 28050a515..9b48caf2d 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; @@ -187,7 +188,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 +252,7 @@ public class GuardedRegion : BaseRegion public void CheckGuardCandidate(Mobile m) { - if (IsDisabled() || !IsGuardCandidate(m)) + if (!IsGuardCandidate(m)) { return; } @@ -276,16 +277,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; } } @@ -308,32 +311,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(); + MakeGuard(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 { From 0a4579f29857a8207ecfc0e358fd7a49d3c70b6c Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Mon, 7 Apr 2025 22:27:41 -0700 Subject: [PATCH 24/53] fix: Fixes duping bags (#2148) ### Summary Fixes duping items in a container not working properly. --- Projects/UOContent/Commands/Dupe.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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); } From fdf8c5cf230cf8b8006f117734949bfa7748c4a8 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:46:53 -0700 Subject: [PATCH 25/53] fix: Fixes critical bug in TickCount calculation. (#2151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary On some operating systems (like hosted Linux VMs), the `TimeStamp.GetTimeStamp()` CPU tick count will wrap around. This is generally not an issue, except for legacy reasons the TickCount is returned in milliseconds instead of ticks. This means when the values wrap around, they are already divided by the CPU Frequency (usually 1million) and then converted to milliseconds. That means the delta between the tick count before and after wrapping is off by a magnitude of (Frequency / 1000). Example: TimeStamp A = 9223372036654775807 Some time has passed: TimeStamp B = -9223372036654775809 The raw delta is 400_000_000 (400ms) when you do `unchecked(A - B)`. If we do the calculation AFTER converting it to milliseconds, then: TickCount A = 9223372036654 TickCount B = -9223372036654 The raw delta is -18446744073308 instead of 400_000_000. To fix this the calculation was changed so `long` -> `ulong`, then divided, then converted back to `long`, effectively bypassing wrap-around issue. The new TickCount values in our example become: TickCount A = 9223372037054 TickCount B = 9223372036654 The delta is 400 (in milliseconds). 🎉 --- Projects/Server/Main.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index d25827f6a..d5eae2b32 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -346,8 +346,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) { From 0ce2a62a76c1d9f511f3e6531b0b27e9c00f5e82 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:40:06 -0700 Subject: [PATCH 26/53] fix: Use vectorized search for username-password validation. (#2152) ### Summary Optimizes account validation: ```cs | Method | Mean | Error | StdDev | |-------------------- |-----------:|----------:|----------:| | ForLoopUsernameSafe | 158.536 ns | 1.6247 ns | 1.5198 ns | | SVUsernameSafe | 2.859 ns | 0.0581 ns | 0.0796 ns | ``` --- .../Tests/Accounting/AccountHandlerTests.cs | 35 ++++++++ .../Security/PasswordProtectionTest.cs | 90 +++++++++---------- .../UOContent/Accounting/AccountHandler.cs | 67 ++++++-------- 3 files changed, 105 insertions(+), 87 deletions(-) create mode 100644 Projects/UOContent.Tests/Tests/Accounting/AccountHandlerTests.cs 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/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) From a94814a7ef5923ab125cdb0d12ead38c760233bf Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:26:10 -0700 Subject: [PATCH 27/53] fix: Fixes and optimizes NameVerification and ProfanityProtection (#2153) ### Summary Significantly improves the performance of NameVerification & ProfanityProtection: ```cs | Method | Mean | Error | StdDev | |--------------- |----------:|----------:|----------:| | ValidateName | 728.34 ns | 13.244 ns | 11.059 ns | | ValidateNameSV | 26.62 ns | 0.233 ns | 0.207 ns | ``` --- .../Tests/Misc/NameVerificationTests.cs | 137 +++++ .../Tests/Misc/ProfanityProtectionTests.cs | 53 ++ .../Character Creation/CharacterCreation.cs | 2 +- .../Guilds/New Guild System/BaseGuildGump.cs | 64 +-- .../UOContent/Items/Deeds/NameChangeDeed.cs | 7 +- Projects/UOContent/Misc/NameVerification.cs | 475 ++++++++++-------- .../UOContent/Misc/ProfanityProtection.cs | 253 +++++----- Projects/UOContent/Misc/RenameRequests.cs | 65 +-- .../UOContent/Mobiles/Vendors/PlayerVendor.cs | 8 +- 9 files changed, 624 insertions(+), 440 deletions(-) create mode 100644 Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs create mode 100644 Projects/UOContent.Tests/Tests/Misc/ProfanityProtectionTests.cs 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/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/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/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/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/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/Mobiles/Vendors/PlayerVendor.cs b/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs index 2721fadbd..d95bcf972 100644 --- a/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs +++ b/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs @@ -1232,9 +1232,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 +1261,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; From 872de8d095ebe3126c4f03162ff2f64c62886f79 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:42:15 -0700 Subject: [PATCH 28/53] fix: Optimizes GetString to eliminate allocations (#2154) ### Summary * Optimized GetString by eliminating the intermediate string allocation. ** Note **: Encoding.GetChars() is still really inefficient, especially when strings are not aligned or have regular ascii/unicode characters. Thankfully we generally don't have to worry about these odd edge cases, but if they happen then GetChars can allocate hundreds of bytes. This is a benchmark for just the related changes. NonSpecial are just ascii characters, while the other tests include control codes. ```cs | Method | Mean | Error | StdDev | Gen0 | Allocated | |------------------------------- |---------:|--------:|--------:|-------:|----------:| | GetString | 306.7 ns | 5.96 ns | 6.86 ns | 0.0124 | 200 B | | GetStringNotSpecial | 257.0 ns | 4.83 ns | 4.52 ns | 0.0114 | 184 B | | GetStringSpanHelpers | 166.4 ns | 3.26 ns | 3.48 ns | - | - | | GetStringSpanHelpersNotSpecial | 137.3 ns | 1.57 ns | 1.22 ns | - | - | ``` --- Projects/Server/Buffers/ValueStringBuilder.cs | 2 +- Projects/Server/Text/TextEncoding.cs | 57 +++++++++++++------ 2 files changed, 41 insertions(+), 18 deletions(-) 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/Text/TextEncoding.cs b/Projects/Server/Text/TextEncoding.cs index bf8051d71..77cc707c1 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,55 @@ 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]); + chars = chars[(index + 1)..]; + index = chars.IndexOfAnyExceptInRange((char)0x20, (char)0xFFFD); + } + + if (chars.Length > 0) + { + sb.Append(chars); + } + + return sb.ToString(); + } + finally + { + if (rentedChars != null) + { + STArrayPool.Shared.Return(rentedChars); } } - - return !hasDoneAnyReplacements ? s : sb.ToString(); } } From 5a23d9f6f8a1d47694f8abb60e9939873455e33f Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:52:10 -0700 Subject: [PATCH 29/53] fix: Fixes an edge case in GetString that can cause a crash while parsing (#2155) --- Projects/Server/Text/TextEncoding.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Projects/Server/Text/TextEncoding.cs b/Projects/Server/Text/TextEncoding.cs index 77cc707c1..b2f290f78 100644 --- a/Projects/Server/Text/TextEncoding.cs +++ b/Projects/Server/Text/TextEncoding.cs @@ -147,8 +147,16 @@ public static class TextEncoding while (index != -1) { sb.Append(chars[..index]); - chars = chars[(index + 1)..]; - index = chars.IndexOfAnyExceptInRange((char)0x20, (char)0xFFFD); + + if (index + 1 < chars.Length) + { + chars = chars[(index + 1)..]; + index = chars.IndexOfAnyExceptInRange((char)0x20, (char)0xFFFD); + } + else + { + index = -1; + } } if (chars.Length > 0) From 61d074213b2d063a056567984ce81cf6a22998a9 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:54:40 -0700 Subject: [PATCH 30/53] chore(deps): Updates MailKit to 4.11.0 and Microsoft deps to 9.0.4 (#2156) --- Projects/Server/Server.csproj | 2 +- Projects/UOContent/UOContent.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index 2fb3d72ef..f7853ec8d 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -37,7 +37,7 @@ - + diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index 535dfcd13..a1146db1c 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -39,8 +39,8 @@ false - - + + From 1e349f4369f2b3914d30c15d92852942d878993e Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 13 Apr 2025 22:55:44 -0700 Subject: [PATCH 31/53] fix: Fixes mutate speech character limit (#2157) ### Summary * Fixes the accidental limitation of dead character speech (OoOo) to 256 characters. * Optimizes it by 1.5x ```cs | Method | Mean | Error | StdDev | Allocated | |------------------------ |----------:|---------:|---------:|----------:| | ManuaLoopMutation | 147.01 ns | 1.298 ns | 1.084 ns | - | | SpanLoopMutation | 92.03 ns | 0.583 ns | 0.487 ns | - | ``` --- Projects/Server/Mobiles/Mobile.cs | 53 +++++++++++++++---------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 66064b87b..e82692ab0 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -29,6 +29,7 @@ using Server.Text; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using Server.Buffers; using CalcMoves = Server.Movement.Movement; namespace Server; @@ -5371,13 +5372,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 +5438,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 +5490,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 +7987,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); From 4aa272d4293af7719a3a375bbea19f0eaf0172a7 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 15 Apr 2025 19:14:45 -0700 Subject: [PATCH 32/53] feat: Adds an Item/Mobile memory leak detector. Fixes minor leak in doors. (#2159) ### Summary Adds the command [TrackLeaks to enable tracking item/mobiles that have been deleted but still have dangling references. Requires adding the _TRACK_LEAKS_ define constant during build. --- Projects/Server/EntityFinalizationTracker.cs | 177 ++++++++++++++++++ .../GarbageCollection/Gen2GcCallback.cs | 48 ++++- Projects/Server/Items/Item.cs | 9 +- Projects/Server/Mobiles/Mobile.cs | 7 + Projects/Server/World/World.cs | 5 + .../Items/SchmendrickApprenticeCorpse.cs | 4 + .../Items/Construction/Doors/BaseDoor.cs | 13 ++ .../UOContent/Items/Misc/Corpses/Corpse.cs | 2 +- docs/commands/commands.7z | Bin 12687 -> 12498 bytes 9 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 Projects/Server/EntityFinalizationTracker.cs diff --git a/Projects/Server/EntityFinalizationTracker.cs b/Projects/Server/EntityFinalizationTracker.cs new file mode 100644 index 000000000..75d4efa9c --- /dev/null +++ b/Projects/Server/EntityFinalizationTracker.cs @@ -0,0 +1,177 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: EntityFinalizationTracker.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +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/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/Item.cs b/Projects/Server/Items/Item.cs index 0fd0dc0bd..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; } @@ -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/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index e82692ab0..2650ef58e 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -4574,6 +4574,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. /// 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/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/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/Misc/Corpses/Corpse.cs b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs index 8ea7d1319..4c093f8f0 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; diff --git a/docs/commands/commands.7z b/docs/commands/commands.7z index d0d6c0e9bbb930aa761d0a8231b619b1ad87724e..6597b7832dec5c20d9e7267513bb02db730bb83d 100644 GIT binary patch delta 9840 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 W5_SUvXmqaI0Tl)T03ZMW0001b`yHtO delta 10030 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 E0DdBdJOBUy From e54782441e3199746d41ea02665a3a01d1f5e10d Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 15 Apr 2025 19:29:17 -0700 Subject: [PATCH 33/53] fix: Fixes TcpServer edge cases where packets are split on login (#2160) --- Projects/Server/Mobiles/Mobile.cs | 1 - Projects/Server/Network/TcpServer.cs | 26 +++++++++++++------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 2650ef58e..a3cd7620d 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -25,7 +25,6 @@ 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; diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index 82bbb2ddd..20c97f792 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -14,7 +14,6 @@ *************************************************************************/ using System; -using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Net; @@ -157,27 +156,33 @@ public static class TcpServer } } + [ThreadStatic] + private static byte[] _firstBytes; + private static async ValueTask ProcessSocketConnection(Socket socket) { - byte[] firstBytes = ArrayPool.Shared.Rent(83); + _firstBytes ??= GC.AllocateUninitializedArray(128); - var cts = CancellationTokenSource.CreateLinkedTokenSource(Core.ClosingTokenSource.Token); + 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 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 || // Older clients only send the 4 byte seed first then 0x80 - (UOClient.MinRequired == null || UOClient.MinRequired < ClientVersion.Version6050) && ( - bytesRead == 4 || bytesRead >= 66 && firstBytes[4] == 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 >= 83 && _firstBytes[0] == 0xEF && _firstBytes[21] == 0x80 || + bytesRead == 21 && _firstBytes[0] == 0xEF || // Game Login - 4 bytes + 0x91 (69 bytes) - bytesRead >= 69 && firstBytes[4] == 0x91 + bytesRead >= 69 && _firstBytes[4] == 0x91 ); // TODO: Validate client version is v4 -> v7 for 0xEF packet @@ -212,11 +217,6 @@ public static class TcpServer { ForceCloseSocket(socket); } - finally - { - ArrayPool.Shared.Return(firstBytes); - cts.Dispose(); - } } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 47b03f5d6246dc958530d2ed1218fc1a03189886 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Mon, 21 Apr 2025 14:01:08 -0700 Subject: [PATCH 34/53] fix: Fixes NPCs not able to equip armor/clothing/weapons (#2161) --- Projects/UOContent/Items/Armor/BaseArmor.cs | 92 ++++++++++--------- .../UOContent/Items/Clothing/BaseClothing.cs | 84 ++++++++--------- .../UOContent/Items/Weapons/BaseWeapon.cs | 5 + 3 files changed, 95 insertions(+), 86 deletions(-) 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/Weapons/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs index 3bc381b5a..23af56d5f 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; From 4e25c74cdf327272514b7298d15f3d65a659b866 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:52:09 -0700 Subject: [PATCH 35/53] fix: Fixes magical barrier NPE and an edge case (#2162) --- .../Mobiles/Abilities/MagicalBarrier.cs | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) 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) From 78feea86b414f0d77e43efdc4496bec75ec6b6c1 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 26 Apr 2025 22:27:08 -0700 Subject: [PATCH 36/53] feat: Adds scheduler with wallclock timer (#2163) ### Summary * Adds an event scheduler. * Adds conveniences for hourly, daily, weekly, biweekly, monthly, ordinal monthly, and yearly recurrences Example: ```cs var tz = TimeZoneInfo.FindSystemTimeZoneById("America/New_York"); // Specify the time of the day, and the day of the week you want it to occur. Make sure it is translated into Utc. // The next occurrence will be _after_ the specified date/time. var scheduledEvent = EventScheduler.WeeklyAt(new DateTime(2025, 04, 26, 17, 00, 00), StartEvent, tz); void StartEvent() { World.Broadcast(0x30, false, "The event has started!"); } Console.WriteLine("Event starts on {0}", scheduledEvent.NextOccurrence); ``` In this example, on _Saturday, May 3rd, 2025 @ 5pm ET_, the message "The event has started!" will be broadcasted. --- .github/workflows/build-test.yml | 2 +- Projects/Server/Main.cs | 3 +- Projects/Server/Server.csproj | 3 + Projects/Server/Utilities/Utility.cs | 17 ++ .../Engines/Events/EventSchedulerTests.cs | 280 ++++++++++++++++++ .../Engines/Events/BroadcastEvent.cs | 30 -- .../Engines/Events/CallbackScheduledEvent.cs | 28 ++ .../Events/CommonRecurrencePatterns.cs | 171 +++++++++++ .../Engines/Events/EventScheduler.cs | 248 ++++++++++++---- 9 files changed, 686 insertions(+), 96 deletions(-) create mode 100644 Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs delete mode 100644 Projects/UOContent/Engines/Events/BroadcastEvent.cs create mode 100644 Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs create mode 100644 Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs 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/Projects/Server/Main.cs b/Projects/Server/Main.cs index d5eae2b32..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; diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index f7853ec8d..9142059c6 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -47,5 +47,8 @@ <_Parameter1>Server.Tests + + <_Parameter1>UOContent.Tests + diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 9f61d7446..50dd43de7 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -1468,4 +1468,21 @@ public static partial class Utility table.Remove(key); table.Add(key, value); } + + public static DateTime LocalToUtc(this DateTime local, TimeZoneInfo tz) + { + if (tz.IsInvalidTime(local)) + { + // For hourly recurrence, just subtract the standard offset (simulate as if the time exists) + return DateTime.SpecifyKind(local - tz.BaseUtcOffset, DateTimeKind.Utc); + } + + 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/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs new file mode 100644 index 000000000..fe5d0c280 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using Server; +using Server.Engines.Events; +using Xunit; + +namespace UOContent.Tests; + +[Collection("Sequential Tests")] +public class EventSchedulerTests +{ + private static void Init() + { + Core._now = DateTime.UtcNow; + Timer.Init(0); + EventScheduler.Configure(); + } + + private static void Finish() + { + EventScheduler.Instance.Stop(); + } + + [Fact] + public void ScheduleEvent_ExecutesCallback_HappyPath() + { + Init(); + + try + { + + } + finally + { + bool called = false; + var evt = EventScheduler.Instance.ScheduleEvent(Core._now, () => called = true); + + Timer.Slice(8); + + Assert.True(called); + Assert.Equal(Core._now, evt.NextOccurrence); + } + + Finish(); + } + + [Theory] + [InlineData(2024, 3, 10, 2, 0, "America/New_York")] // DST spring forward gap (invalid) + [InlineData(2024, 11, 3, 1, 0, "America/New_York")] // DST fall back (ambiguous) + [InlineData(2024, 6, 1, 5, 0, "America/New_York")] // Normal time + public void HourlyRecurrence( + int year, int month, int day, int hour, int minute, string tzId) + { + Init(); + + try + { + var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId); + var local = new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Unspecified); + + // Set Core._now to just before the target hour + var beforeLocal = local.AddHours(-1); + Core._now = beforeLocal.LocalToUtc(tz); + + bool called = false; + var evt = EventScheduler.Instance.ScheduleEvent( + Core._now, + () => called = true, + EventScheduler.Hourly, + tz + ); + + // Advance to the target hour + Core._now = local.LocalToUtc(tz); + Assert.Equal(Core._now, evt.NextOccurrence); + Timer.Slice(8); + + // Hourly recurrence should always execute, even in DST gaps/ambiguous times + Assert.True(called); + Assert.Equal(Core._now.AddHours(1), evt.NextOccurrence); + } + finally + { + Finish(); + } + } + + [Theory] + [InlineData(2024, 3, 10, 2, 0, "America/New_York")] // DST spring forward gap + [InlineData(2024, 11, 3, 1, 30, "America/New_York")] // DST fall back + [InlineData(2024, 6, 2, 5, 0, "America/New_York")] // Normal time + public void MonthlyRecurrence( + int year, int month, int day, int hour, int minute, string tzId) + { + Init(); + + try + { + var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId); + var local = new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Unspecified); + + Core._now = local.AddDays(-1).LocalToUtc(tz); + + bool called = false; + var evt = EventScheduler.Instance.ScheduleEvent( + Core._now, + () => called = true, + EventScheduler.GetMonthlyRecurrence(day), + tz + ); + + Core._now = local.LocalToUtc(tz); + var invalidTime = tz.IsInvalidTime(local); + + // Invalid time ranges are not executed, so the occurrence is an additional month later + Assert.Equal(invalidTime ? local.AddMonths(1).LocalToUtc(tz) : Core._now, evt.NextOccurrence); + + Timer.Slice(8); + if (invalidTime) + { + Assert.False(called); + } + else + { + Assert.True(called); + } + } + finally + { + Finish(); + } + } + + [Theory] + [InlineData(2024, 11, 3, 1, 30, DayOfWeek.Sunday, OrdinalDayOccurrence.First, "America/New_York")] // DST fallback + [InlineData(2024, 3, 31, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Last, "America/New_York")] // Last Sunday + [InlineData(2024, 3, 31, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Fifth, "America/New_York")] // Fifth Sunday + [InlineData(2024, 4, 3, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Fifth, "America/New_York")] // Fifth Sunday (Doesn't exist) + public void MonthlyOrdinalRecurrence( + int year, int month, int day, int hour, int minute, DayOfWeek dow, OrdinalDayOccurrence ordinal, string tzId) + { + Init(); + + try + { + var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId); + var local = new DateTime(year, month, day, hour, minute, 0).AddDays(-1); + Core._now = local.LocalToUtc(tz); + + bool called = false; + var pattern = new MonthlyOrdinalRecurrencePattern(ordinal, dow); + var evt = EventScheduler.Instance.ScheduleEvent( + Core._now, + () => called = true, + pattern, + tz + ); + + var testTime = Core._now.AddDays(1); + + if (month == 4 && ordinal == OrdinalDayOccurrence.Fifth) + { + // If there is no fifth occurrence, NextOccurrence should be in the next month + var expectedNext = pattern.GetNextOccurrence(Core._now, tz); + Assert.Equal(expectedNext, evt.NextOccurrence); + Core._now = testTime; + Timer.Slice(8); + Assert.False(called); + } + else + { + Assert.Equal(testTime, evt.NextOccurrence); + Core._now = testTime; + Timer.Slice(8); + Assert.True(called); + } + } + finally + { + Finish(); + } + } + + [Fact] + public void ScheduleEvent_NullCallback_Throws() + { + Init(); + + try + { + Assert.Throws(() => + EventScheduler.Instance.ScheduleEvent(Core._now, null) + ); + } + finally + { + Finish(); + } + } + + [Fact] + public void AdvanceEvent_PastEndDate_DoesNotReschedule() + { + Init(); + + try + { + bool called = false; + var evt = new CallbackScheduledEvent(Core._now, Core._now, () => called = true); + + EventScheduler.Instance.ScheduleEvent(evt); + + Timer.Slice(8); + Assert.True(called); + + Assert.DoesNotContain( + evt, + typeof(EventScheduler) + .GetField( + "_schedule", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance + )! + .GetValue(EventScheduler.Instance) as IEnumerable ?? [] + ); + } + finally + { + Finish(); + } + } + + [Theory] + [InlineData("UTC")] // No DST adjustments + [InlineData("Asia/Kathmandu")] // Unusual offset (UTC+5:45) + public void LocalToUtc_SpecialTimeZones_HandlesCorrectly(string tzId) + { + Init(); + + try + { + var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId); + var local = new DateTime(2024, 6, 1, 12, 0, 0, DateTimeKind.Unspecified); + var expected = TimeZoneInfo.ConvertTimeToUtc(local, tz); + + var actual = local.LocalToUtc(tz); + + Assert.Equal(expected, actual); + Assert.Equal(DateTimeKind.Utc, actual.Kind); + } + finally + { + Finish(); + } + } + + [Fact] + public void LocalToUtc_EdgeCases_BeforeAndAfterTransition() + { + Init(); + + try + { + var tz = TimeZoneInfo.FindSystemTimeZoneById("America/New_York"); + // 1:59 AM (just before spring forward) + var beforeSpring = new DateTime(2024, 3, 10, 1, 59, 0); + // 3:01 AM (just after spring forward) + var afterSpring = new DateTime(2024, 3, 10, 3, 1, 0); + + var beforeUtc = beforeSpring.LocalToUtc(tz); + var afterUtc = afterSpring.LocalToUtc(tz); + + // Should be 1 hour + 2 minutes apart in UTC (not 1 hour 2 minutes) + Assert.Equal(2, (afterUtc - beforeUtc).TotalMinutes); + } + finally + { + Finish(); + } + } +} 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..1034314f3 --- /dev/null +++ b/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs @@ -0,0 +1,28 @@ +using System; + +namespace Server.Engines.Events; + +public sealed class CallbackScheduledEvent : ScheduledEvent +{ + private readonly Action _callback; + + public CallbackScheduledEvent( + DateTime afterUtc, + Action callback, + IRecurrencePattern recurrencePattern = null, + TimeZoneInfo timeZone = null + ) : base(afterUtc, recurrencePattern, timeZone) => + _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + + public CallbackScheduledEvent( + DateTime afterUtc, + DateTime endDate, + Action callback, + IRecurrencePattern recurrencePattern = null, + TimeZoneInfo timeZone = null + ) : base(afterUtc, endDate, recurrencePattern, timeZone) => + _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..5b13bb07e --- /dev/null +++ b/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs @@ -0,0 +1,171 @@ +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, TimeZoneInfo timeZone) => afterUtc.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, TimeZoneInfo timeZone) => afterUtc.AddDays(IntervalDays); +} + +public class WeeklyRecurrencePattern : IRecurrencePattern +{ + public int IntervalWeeks { get; } + public DaysOfWeek DaysOfWeek { get; } + + public WeeklyRecurrencePattern(int intervalWeeks = 1, DaysOfWeek daysOfWeek = DaysOfWeek.None) + { + IntervalWeeks = Math.Max(1, intervalWeeks); + DaysOfWeek = daysOfWeek; + } + + public DateTime GetNextOccurrence(DateTime afterUtc, TimeZoneInfo timeZone) + { + var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); + var weekStart = local.Date.AddDays(1); + var daysOfWeek = DaysOfWeek; + + // Set the day of the week to whatever day it is now + if (daysOfWeek == DaysOfWeek.None) + { + daysOfWeek = (DaysOfWeek)(1 << (int)local.DayOfWeek); + } + + // Example: + // Recurrence is Monday, Wednesday, Friday - and today is Wednesday + // weekStart will be Thursday, and then we check every day for 7 days to find the next occurrence match. + for (int i = 0; i < 7; i++) + { + var candidate = weekStart.AddDays(i); + var candidateDay = (DaysOfWeek)(1 << (int)candidate.DayOfWeek); + + if ((daysOfWeek & candidateDay) != 0 && 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, TimeZoneInfo timeZone) + { + var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); + var year = local.Year; + var month = local.Month; + var time = local.TimeOfDay; + var day = DayOfMonth == -1 ? local.Day : DayOfMonth; + + for (int 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); + + // 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, TimeZoneInfo timeZone) + { + var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); + var year = local.Year; + var month = local.Month; + + for (int 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, local.Hour, local.Minute, local.Second); + + // Find the first occurrence of the desired day + int 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, local.Hour, local.Minute, local.Second); + + // Find the last occurrence of the desired day + int 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..a5e04f55f 100644 --- a/Projects/UOContent/Engines/Events/EventScheduler.cs +++ b/Projects/UOContent/Engines/Events/EventScheduler.cs @@ -1,95 +1,215 @@ 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, TimeZoneInfo timeZone); +} + +public enum OrdinalDayOccurrence { Last = -1, First, Second, Third, Fourth, Fifth } + +[Flags] +public enum DaysOfWeek : byte +{ + None = 0, + Sunday = 1, + Monday = 2, + Tuesday = 4, + Wednesday = 8, + Thursday = 16, + Friday = 32, + Saturday = 64, + EveryDay = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday +} + +public abstract class ScheduledEvent +{ + private static Serial _nextSerial = (Serial)1; + + // Tie breaker for sorted set + public Serial Serial { get; } + public IRecurrencePattern Recurrence { get; } + public TimeZoneInfo TimeZone { get; } + public DateTime EndDate { get; } + public DateTime NextOccurrence { get; private set; } + + public ScheduledEvent(DateTime startOn, TimeZoneInfo timeZone = null) + : this(startOn, startOn, null, timeZone) { - void OnEventScheduled(); } - public class EventScheduleEntry + public ScheduledEvent(DateTime afterUtc, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null) + : this(afterUtc, DateTime.MaxValue, recurrence, timeZone) { - 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(); } - public class EventScheduler : Timer + public ScheduledEvent( + DateTime afterUtc, + DateTime endDateUtc, + IRecurrencePattern recurrence, + TimeZoneInfo timeZone = null + ) { - private static EventScheduler _instance; - private readonly List _schedule = new(); + Serial = _nextSerial++; + Recurrence = recurrence; + TimeZone = timeZone ?? TimeZoneInfo.Utc; + NextOccurrence = recurrence?.GetNextOccurrence(afterUtc, TimeZone) ?? afterUtc; + EndDate = endDateUtc; + } - private EventScheduler() : base(TimeSpan.Zero, TimeSpan.FromSeconds(1.0)) + public bool Advance() + { + OnEvent(); + + var next = Recurrence?.GetNextOccurrence(NextOccurrence, TimeZone) ?? DateTime.MaxValue; + if (next == DateTime.MaxValue || next > EndDate) { + return false; } - public static EventScheduler Instance => _instance ??= new EventScheduler(); - public static List AvailableEvents { get; } = new(); + NextOccurrence = next; + return true; + } - public static void Initialize() + public abstract void OnEvent(); +} + +public class EventScheduler : Timer +{ + private readonly SortedSet _schedule = new(ScheduledEventComparer.Default); + + public static EventScheduler Instance { 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) + { + ref var pattern = ref CollectionsMarshal.GetValueRefOrAddDefault(_monthlyRecurrenceByDay, dayOfMonth, out var exists); + if (!exists) { - Instance.Start(); + pattern = new MonthlyRecurrencePattern(dayOfMonth); } - public void ScheduleEvent(IEvent e, int hour, int min) - { - ScheduleEvent(e, hour, min, TimeSpan.FromDays(1.0)); - } + return pattern; + } - 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); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent HourlyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Instance.ScheduleEvent(startOn, action, Hourly, timeZone); - while (now > firstRun) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent DailyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Instance.ScheduleEvent(startOn, action, Daily, timeZone); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent WeeklyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Instance.ScheduleEvent(startOn, action, Weekly, timeZone); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent BiweeklyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Instance.ScheduleEvent(startOn, action, Biweekly, timeZone); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent MonthlyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Instance.ScheduleEvent(startOn, action, GetMonthlyRecurrence(startOn.Day), timeZone); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScheduledEvent YearlyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => + Instance.ScheduleEvent(startOn, action, GetMonthlyRecurrence(startOn.Day), timeZone); + + public static void Configure() + { + Instance ??= new EventScheduler(); + Instance.Start(); + } + + private EventScheduler() : base(TimeSpan.Zero, TimeSpan.FromSeconds(1.0)) + { + } + + public ScheduledEvent ScheduleEvent( + DateTime afterUtc, + Action callback, + IRecurrencePattern recurrencePattern = null, + TimeZoneInfo timeZone = null + ) + { + var scheduledEvent = new CallbackScheduledEvent(afterUtc, callback, recurrencePattern, timeZone); + ScheduleEvent(scheduledEvent); + return scheduledEvent; + } + + public void ScheduleEvent(ScheduledEvent e) => _schedule.Add(e); + + public void StopEvent(ScheduledEvent entry) => _schedule.Remove(entry); + + protected override void OnTick() + { + var now = Core.Now; + + while (_schedule.Count > 0) + { + var entry = _schedule.Min!; + if (entry.NextOccurrence > now) { - firstRun += interval; + break; } - ScheduleEvent( - new EventScheduleEntry(e, firstRun, interval, TimeSpan.FromHours(hour) + TimeSpan.FromMinutes(min)) - ); - } - - 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 (entry.Advance() && entry.NextOccurrence < DateTime.MaxValue) { - if (entry.NextOccurrence <= Core.Now) - { - entry.Occur(); - } + _schedule.Add(entry); } } } + + private sealed class ScheduledEventComparer : IComparer + { + public static readonly ScheduledEventComparer Default = new(); + + public int Compare(ScheduledEvent x, ScheduledEvent y) + { + if (x == null && y == null) + { + return 0; + } + + if (x == null) + { + return 1; + } + + if (y == null) + { + return -1; + } + + var next = x.NextOccurrence.CompareTo(y.NextOccurrence); + return next != 0 ? next : x.Serial.CompareTo(y.Serial); + } + } } From f89b2be65391a39d12d9414f5911f77adf755749 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 27 Apr 2025 22:05:07 -0700 Subject: [PATCH 37/53] fix: Fixes major bug with BoBEntries removal (#2165) --- Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From 21a4092dd826a5d20afa95908caa653fe84810f4 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:16:33 -0700 Subject: [PATCH 38/53] fix: Changes EventScheduler API so it is more explicit (#2164) ### Summary Refactors ScheduledEvent and EventScheduler API to use TimeOnly so recurrence offset is explicit. API: ```cs public ScheduledEvent( DateTime startAfter, DateTime endOn, TimeOnly time, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null ) ``` Example: ```cs // Schedule a daily event at 8:00 AM UTC EventScheduler.DailyAt( new DateTime(2024, 6, 1, 8, 0, 0, DateTimeKind.Utc), () => Console.WriteLine("Daily event triggered!") ); // Schedule a custom recurring event at 3:30 PM UTC every Monday var recurrence = new WeeklyRecurrencePattern(1, DaysOfWeek.Monday); EventScheduler.Shared.ScheduleEvent( DateTime.UtcNow, new TimeOnly(15, 30), () => Console.WriteLine("Weekly Monday event!"), recurrence ); ``` --- .github/workflows/code_quality.yml | 2 +- Projects/Server/Utilities/Utility.cs | 6 - .../Engines/Events/EventSchedulerTests.cs | 432 ++++++++++-------- .../Engines/Events/RecurrencePatternTests.cs | 168 +++++++ .../Engines/Events/CallbackScheduledEvent.cs | 13 +- .../Events/CommonRecurrencePatterns.cs | 37 +- .../Engines/Events/EventScheduler.cs | 87 +++- 7 files changed, 495 insertions(+), 250 deletions(-) create mode 100644 Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs 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/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 50dd43de7..6ef4aa72c 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -1471,12 +1471,6 @@ public static partial class Utility public static DateTime LocalToUtc(this DateTime local, TimeZoneInfo tz) { - if (tz.IsInvalidTime(local)) - { - // For hourly recurrence, just subtract the standard offset (simulate as if the time exists) - return DateTime.SpecifyKind(local - tz.BaseUtcOffset, DateTimeKind.Utc); - } - if (tz.IsAmbiguousTime(local)) { var offsets = tz.GetAmbiguousTimeOffsets(local); diff --git a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs index fe5d0c280..d44116a3b 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs @@ -9,16 +9,62 @@ namespace UOContent.Tests; [Collection("Sequential 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(DateTime startAfter, Action callback, IRecurrencePattern recurrence = null) + : base(startAfter, TimeOnly.FromDateTime(startAfter), recurrence, TimeZoneInfo.Utc) + { + CallCount = 0; + Callback = callback; + } + + public override void OnEvent() + { + CallCount++; + Callback?.Invoke(); + } + } + private static void Init() { - Core._now = DateTime.UtcNow; + Core._now = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc); Timer.Init(0); EventScheduler.Configure(); } private static void Finish() { - EventScheduler.Instance.Stop(); + EventScheduler.Shared.Stop(); } [Fact] @@ -27,153 +73,20 @@ public class EventSchedulerTests Init(); try - { - - } - finally { bool called = false; - var evt = EventScheduler.Instance.ScheduleEvent(Core._now, () => called = true); + var evt = EventScheduler.Shared.ScheduleEvent( + Core._now, + TimeOnly.FromDateTime(Core._now), + () => called = true + ); Timer.Slice(8); Assert.True(called); Assert.Equal(Core._now, evt.NextOccurrence); - } - Finish(); - } - - [Theory] - [InlineData(2024, 3, 10, 2, 0, "America/New_York")] // DST spring forward gap (invalid) - [InlineData(2024, 11, 3, 1, 0, "America/New_York")] // DST fall back (ambiguous) - [InlineData(2024, 6, 1, 5, 0, "America/New_York")] // Normal time - public void HourlyRecurrence( - int year, int month, int day, int hour, int minute, string tzId) - { - Init(); - - try - { - var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId); - var local = new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Unspecified); - - // Set Core._now to just before the target hour - var beforeLocal = local.AddHours(-1); - Core._now = beforeLocal.LocalToUtc(tz); - - bool called = false; - var evt = EventScheduler.Instance.ScheduleEvent( - Core._now, - () => called = true, - EventScheduler.Hourly, - tz - ); - - // Advance to the target hour - Core._now = local.LocalToUtc(tz); - Assert.Equal(Core._now, evt.NextOccurrence); - Timer.Slice(8); - - // Hourly recurrence should always execute, even in DST gaps/ambiguous times - Assert.True(called); - Assert.Equal(Core._now.AddHours(1), evt.NextOccurrence); - } - finally - { - Finish(); - } - } - - [Theory] - [InlineData(2024, 3, 10, 2, 0, "America/New_York")] // DST spring forward gap - [InlineData(2024, 11, 3, 1, 30, "America/New_York")] // DST fall back - [InlineData(2024, 6, 2, 5, 0, "America/New_York")] // Normal time - public void MonthlyRecurrence( - int year, int month, int day, int hour, int minute, string tzId) - { - Init(); - - try - { - var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId); - var local = new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Unspecified); - - Core._now = local.AddDays(-1).LocalToUtc(tz); - - bool called = false; - var evt = EventScheduler.Instance.ScheduleEvent( - Core._now, - () => called = true, - EventScheduler.GetMonthlyRecurrence(day), - tz - ); - - Core._now = local.LocalToUtc(tz); - var invalidTime = tz.IsInvalidTime(local); - - // Invalid time ranges are not executed, so the occurrence is an additional month later - Assert.Equal(invalidTime ? local.AddMonths(1).LocalToUtc(tz) : Core._now, evt.NextOccurrence); - - Timer.Slice(8); - if (invalidTime) - { - Assert.False(called); - } - else - { - Assert.True(called); - } - } - finally - { - Finish(); - } - } - - [Theory] - [InlineData(2024, 11, 3, 1, 30, DayOfWeek.Sunday, OrdinalDayOccurrence.First, "America/New_York")] // DST fallback - [InlineData(2024, 3, 31, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Last, "America/New_York")] // Last Sunday - [InlineData(2024, 3, 31, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Fifth, "America/New_York")] // Fifth Sunday - [InlineData(2024, 4, 3, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Fifth, "America/New_York")] // Fifth Sunday (Doesn't exist) - public void MonthlyOrdinalRecurrence( - int year, int month, int day, int hour, int minute, DayOfWeek dow, OrdinalDayOccurrence ordinal, string tzId) - { - Init(); - - try - { - var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId); - var local = new DateTime(year, month, day, hour, minute, 0).AddDays(-1); - Core._now = local.LocalToUtc(tz); - - bool called = false; - var pattern = new MonthlyOrdinalRecurrencePattern(ordinal, dow); - var evt = EventScheduler.Instance.ScheduleEvent( - Core._now, - () => called = true, - pattern, - tz - ); - - var testTime = Core._now.AddDays(1); - - if (month == 4 && ordinal == OrdinalDayOccurrence.Fifth) - { - // If there is no fifth occurrence, NextOccurrence should be in the next month - var expectedNext = pattern.GetNextOccurrence(Core._now, tz); - Assert.Equal(expectedNext, evt.NextOccurrence); - Core._now = testTime; - Timer.Slice(8); - Assert.False(called); - } - else - { - Assert.Equal(testTime, evt.NextOccurrence); - Core._now = testTime; - Timer.Slice(8); - Assert.True(called); - } + EventScheduler.Shared.StopEvent(evt); } finally { @@ -182,70 +95,45 @@ public class EventSchedulerTests } [Fact] - public void ScheduleEvent_NullCallback_Throws() + public void Scheduler_OrdersEventsByNextOccurrence() { Init(); try { - Assert.Throws(() => - EventScheduler.Instance.ScheduleEvent(Core._now, null) + var executionOrder = new List(); + + // Create events with staggered occurrences + var evt3 = new TestScheduledEvent( + Core._now.AddSeconds(30), + () => executionOrder.Add(3) ); - } - finally - { - Finish(); - } - } - [Fact] - public void AdvanceEvent_PastEndDate_DoesNotReschedule() - { - Init(); + var evt1 = new TestScheduledEvent( + Core._now.AddSeconds(10), + () => executionOrder.Add(1) + ); - try - { - bool called = false; - var evt = new CallbackScheduledEvent(Core._now, Core._now, () => called = true); + var evt2 = new TestScheduledEvent( + Core._now.AddSeconds(20), + () => executionOrder.Add(2) + ); - EventScheduler.Instance.ScheduleEvent(evt); + // Add out of order + EventScheduler.Shared.ScheduleEvent(evt3); + EventScheduler.Shared.ScheduleEvent(evt1); + EventScheduler.Shared.ScheduleEvent(evt2); + // Advance time to after all events + Core._now = Core._now.AddSeconds(40); Timer.Slice(8); - Assert.True(called); - Assert.DoesNotContain( - evt, - typeof(EventScheduler) - .GetField( - "_schedule", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance - )! - .GetValue(EventScheduler.Instance) as IEnumerable ?? [] - ); - } - finally - { - Finish(); - } - } + // Verify they executed in time order, not addition order + Assert.Equal([1, 2, 3], executionOrder); - [Theory] - [InlineData("UTC")] // No DST adjustments - [InlineData("Asia/Kathmandu")] // Unusual offset (UTC+5:45) - public void LocalToUtc_SpecialTimeZones_HandlesCorrectly(string tzId) - { - Init(); - - try - { - var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId); - var local = new DateTime(2024, 6, 1, 12, 0, 0, DateTimeKind.Unspecified); - var expected = TimeZoneInfo.ConvertTimeToUtc(local, tz); - - var actual = local.LocalToUtc(tz); - - Assert.Equal(expected, actual); - Assert.Equal(DateTimeKind.Utc, actual.Kind); + EventScheduler.Shared.StopEvent(evt1); + EventScheduler.Shared.StopEvent(evt2); + EventScheduler.Shared.StopEvent(evt3); } finally { @@ -254,27 +142,167 @@ public class EventSchedulerTests } [Fact] - public void LocalToUtc_EdgeCases_BeforeAndAfterTransition() + public void Scheduler_HandlesRecurringEvents() { Init(); try { - var tz = TimeZoneInfo.FindSystemTimeZoneById("America/New_York"); - // 1:59 AM (just before spring forward) - var beforeSpring = new DateTime(2024, 3, 10, 1, 59, 0); - // 3:01 AM (just after spring forward) - var afterSpring = new DateTime(2024, 3, 10, 3, 1, 0); + int callCount = 0; - var beforeUtc = beforeSpring.LocalToUtc(tz); - var afterUtc = afterSpring.LocalToUtc(tz); + // Create a recurrence pattern that fires every 10 seconds, up to 3 times + var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3); - // Should be 1 hour + 2 minutes apart in UTC (not 1 hour 2 minutes) - Assert.Equal(2, (afterUtc - beforeUtc).TotalMinutes); + var evt = new TestScheduledEvent( + Core._now, + () => callCount++, + recurrence + ); + + EventScheduler.Shared.ScheduleEvent(evt); + + // 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); + + EventScheduler.Shared.StopEvent(evt); } 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); + + EventScheduler.Shared.StopEvent(failedEvt); + EventScheduler.Shared.StopEvent(laterEvt); + } + 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 + EventScheduler.Shared.StopEvent(evt); + + // 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( + startTime, + endTime, + TimeOnly.FromDateTime(startTime), + recurrence, + () => callCount++ + ); + + EventScheduler.Shared.ScheduleEvent(customEvent); + + 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( + DateTime startAfter, + DateTime endOn, + TimeOnly time, + IRecurrencePattern recurrence, + Action callback + ) : base(startAfter, endOn, time, recurrence, TimeZoneInfo.Utc) => _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..7917194c8 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs @@ -0,0 +1,168 @@ +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, 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); + } + + // Additional utility method tests + [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/Engines/Events/CallbackScheduledEvent.cs b/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs index 1034314f3..e142cf247 100644 --- a/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs +++ b/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs @@ -7,22 +7,23 @@ public sealed class CallbackScheduledEvent : ScheduledEvent private readonly Action _callback; public CallbackScheduledEvent( - DateTime afterUtc, + DateTime after, + TimeOnly time, Action callback, IRecurrencePattern recurrencePattern = null, TimeZoneInfo timeZone = null - ) : base(afterUtc, recurrencePattern, timeZone) => + ) : base(after, time, recurrencePattern, timeZone) => _callback = callback ?? throw new ArgumentNullException(nameof(callback)); public CallbackScheduledEvent( - DateTime afterUtc, - DateTime endDate, + DateTime after, + DateTime endOn, + TimeOnly time, Action callback, IRecurrencePattern recurrencePattern = null, TimeZoneInfo timeZone = null - ) : base(afterUtc, endDate, recurrencePattern, timeZone) => + ) : base(after, endOn, time, recurrencePattern, timeZone) => _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 index 5b13bb07e..fb7a11cf4 100644 --- a/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs +++ b/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs @@ -8,7 +8,13 @@ public class HourlyRecurrencePattern : IRecurrencePattern public HourlyRecurrencePattern(int intervalHours = 1) => IntervalHours = Math.Max(1, intervalHours); - public DateTime GetNextOccurrence(DateTime afterUtc, TimeZoneInfo timeZone) => afterUtc.AddHours(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 @@ -17,7 +23,13 @@ public class DailyRecurrencePattern : IRecurrencePattern public DailyRecurrencePattern(int intervalDays = 1) => IntervalDays = Math.Max(1, intervalDays); - public DateTime GetNextOccurrence(DateTime afterUtc, TimeZoneInfo timeZone) => afterUtc.AddDays(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 @@ -31,10 +43,12 @@ public class WeeklyRecurrencePattern : IRecurrencePattern DaysOfWeek = daysOfWeek; } - public DateTime GetNextOccurrence(DateTime afterUtc, TimeZoneInfo timeZone) + public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone) { var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); - var weekStart = local.Date.AddDays(1); + var localDate = DateOnly.FromDateTime(local); + + var weekStart = localDate.ToDateTime(time); var daysOfWeek = DaysOfWeek; // Set the day of the week to whatever day it is now @@ -46,7 +60,7 @@ public class WeeklyRecurrencePattern : IRecurrencePattern // Example: // Recurrence is Monday, Wednesday, Friday - and today is Wednesday // weekStart will be Thursday, and then we check every day for 7 days to find the next occurrence match. - for (int i = 0; i < 7; i++) + for (int i = 1; i < 8; i++) { var candidate = weekStart.AddDays(i); var candidateDay = (DaysOfWeek)(1 << (int)candidate.DayOfWeek); @@ -72,12 +86,11 @@ public class MonthlyRecurrencePattern : IRecurrencePattern IntervalMonths = Math.Max(1, intervalMonths); } - public DateTime GetNextOccurrence(DateTime afterUtc, TimeZoneInfo timeZone) + public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone) { var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); var year = local.Year; var month = local.Month; - var time = local.TimeOfDay; var day = DayOfMonth == -1 ? local.Day : DayOfMonth; for (int i = 0; i < 100; i++) @@ -86,7 +99,7 @@ public class MonthlyRecurrencePattern : IRecurrencePattern var candidate = new DateTime(year, 1, 1) .AddMonths(nextMonth - 1) .AddDays(day - 1) - .Add(time); + .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)) @@ -112,7 +125,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern DayOfWeek = dayOfWeek; } - public DateTime GetNextOccurrence(DateTime afterUtc, TimeZoneInfo timeZone) + public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone) { var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); var year = local.Year; @@ -129,7 +142,8 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern if (Ordinal >= OrdinalDayOccurrence.First) { // Find the first day of the month - var firstOfMonth = new DateTime(candidateYear, candidateMonth, 1, local.Hour, local.Minute, local.Second); + var firstOfMonth = new DateTime(candidateYear, candidateMonth, 1) + .Add(time.ToTimeSpan()); // Find the first occurrence of the desired day int daysOffset = ((int)DayOfWeek - (int)firstOfMonth.DayOfWeek + 7) % 7; @@ -149,7 +163,8 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern { // Find the last day of the month var daysInMonth = DateTime.DaysInMonth(candidateYear, candidateMonth); - var lastOfMonth = new DateTime(candidateYear, candidateMonth, daysInMonth, local.Hour, local.Minute, local.Second); + var lastOfMonth = new DateTime(candidateYear, candidateMonth, daysInMonth) + .Add(time.ToTimeSpan()); // Find the last occurrence of the desired day int daysOffset = (int)lastOfMonth.DayOfWeek - (int)DayOfWeek + 7; diff --git a/Projects/UOContent/Engines/Events/EventScheduler.cs b/Projects/UOContent/Engines/Events/EventScheduler.cs index a5e04f55f..cf681d33e 100644 --- a/Projects/UOContent/Engines/Events/EventScheduler.cs +++ b/Projects/UOContent/Engines/Events/EventScheduler.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Server.Logging; namespace Server.Engines.Events; @@ -11,7 +12,7 @@ public interface IRecurrencePattern /// Get the next occurrence of the event. /// DateTime of the next occurence in UTC or DateTime.MaxValue /// - DateTime GetNextOccurrence(DateTime afterUtc, TimeZoneInfo timeZone); + DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone); } public enum OrdinalDayOccurrence { Last = -1, First, Second, Third, Fourth, Fifth } @@ -34,42 +35,47 @@ public abstract class ScheduledEvent { private static Serial _nextSerial = (Serial)1; - // Tie breaker for sorted set + // Tie-breaker for sorted set public Serial Serial { get; } public IRecurrencePattern Recurrence { get; } public TimeZoneInfo TimeZone { get; } + public TimeOnly Time { get; } public DateTime EndDate { get; } public DateTime NextOccurrence { get; private set; } public ScheduledEvent(DateTime startOn, TimeZoneInfo timeZone = null) - : this(startOn, startOn, null, timeZone) + : this(startOn, startOn, TimeOnly.FromDateTime(startOn), null, timeZone) { } - public ScheduledEvent(DateTime afterUtc, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null) - : this(afterUtc, DateTime.MaxValue, recurrence, timeZone) + public ScheduledEvent(DateTime startAfter, TimeOnly time, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null) + : this(startAfter, DateTime.MaxValue, time, recurrence, timeZone) { } public ScheduledEvent( - DateTime afterUtc, - DateTime endDateUtc, + DateTime startAfter, + DateTime endOn, + TimeOnly time, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null ) { Serial = _nextSerial++; + Time = time; Recurrence = recurrence; TimeZone = timeZone ?? TimeZoneInfo.Utc; - NextOccurrence = recurrence?.GetNextOccurrence(afterUtc, TimeZone) ?? afterUtc; - EndDate = endDateUtc; + + var afterUtc = startAfter.Kind == DateTimeKind.Utc ? startAfter : startAfter.LocalToUtc(TimeZone); + NextOccurrence = recurrence?.GetNextOccurrence(afterUtc, time, TimeZone) ?? afterUtc; + EndDate = endOn == DateTime.MaxValue || endOn.Kind == DateTimeKind.Utc ? endOn : endOn.LocalToUtc(TimeZone); } public bool Advance() { OnEvent(); - var next = Recurrence?.GetNextOccurrence(NextOccurrence, TimeZone) ?? DateTime.MaxValue; + var next = Recurrence?.GetNextOccurrence(NextOccurrence, Time, TimeZone) ?? DateTime.MaxValue; if (next == DateTime.MaxValue || next > EndDate) { return false; @@ -84,9 +90,11 @@ public abstract class ScheduledEvent public class EventScheduler : Timer { + private static readonly ILogger logger = LogFactory.GetLogger(typeof(EventScheduler)); + private readonly SortedSet _schedule = new(ScheduledEventComparer.Default); - public static EventScheduler Instance { get; private set; } + public static EventScheduler Shared { get; private set; } public static IRecurrencePattern Hourly => new HourlyRecurrencePattern(); @@ -118,32 +126,32 @@ public class EventScheduler : Timer [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ScheduledEvent HourlyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => - Instance.ScheduleEvent(startOn, action, Hourly, timeZone); + Shared.ScheduleEvent(startOn, action, Hourly, timeZone); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ScheduledEvent DailyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => - Instance.ScheduleEvent(startOn, action, Daily, timeZone); + Shared.ScheduleEvent(startOn, action, Daily, timeZone); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ScheduledEvent WeeklyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => - Instance.ScheduleEvent(startOn, action, Weekly, timeZone); + Shared.ScheduleEvent(startOn, action, Weekly, timeZone); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ScheduledEvent BiweeklyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => - Instance.ScheduleEvent(startOn, action, Biweekly, timeZone); + Shared.ScheduleEvent(startOn, action, Biweekly, timeZone); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ScheduledEvent MonthlyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => - Instance.ScheduleEvent(startOn, action, GetMonthlyRecurrence(startOn.Day), timeZone); + Shared.ScheduleEvent(startOn, action, GetMonthlyRecurrence(startOn.Day), timeZone); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ScheduledEvent YearlyAt(DateTime startOn, Action action, TimeZoneInfo timeZone = null) => - Instance.ScheduleEvent(startOn, action, GetMonthlyRecurrence(startOn.Day), timeZone); + Shared.ScheduleEvent(startOn, action, GetMonthlyRecurrence(startOn.Day), timeZone); public static void Configure() { - Instance ??= new EventScheduler(); - Instance.Start(); + Shared ??= new EventScheduler(); + Shared.Start(); } private EventScheduler() : base(TimeSpan.Zero, TimeSpan.FromSeconds(1.0)) @@ -151,20 +159,40 @@ public class EventScheduler : Timer } public ScheduledEvent ScheduleEvent( - DateTime afterUtc, + 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(afterUtc, callback, recurrencePattern, timeZone); + var scheduledEvent = new CallbackScheduledEvent(after, time, callback, recurrencePattern, timeZone); ScheduleEvent(scheduledEvent); return scheduledEvent; } - public void ScheduleEvent(ScheduledEvent e) => _schedule.Add(e); + public void ScheduleEvent(ScheduledEvent entry) + { + if (entry != null) + { + _schedule.Add(entry); + } + } - public void StopEvent(ScheduledEvent entry) => _schedule.Remove(entry); + public void StopEvent(ScheduledEvent entry) + { + if (entry != null) + { + _schedule.Remove(entry); + } + } protected override void OnTick() { @@ -180,7 +208,18 @@ public class EventScheduler : Timer _schedule.Remove(entry); - if (entry.Advance() && entry.NextOccurrence < DateTime.MaxValue) + bool advance; + try + { + advance = entry.Advance(); + } + catch (Exception e) + { + logger.Error(e, "Error while executing scheduled event."); + advance = false; + } + + if (advance && entry.NextOccurrence < DateTime.MaxValue) { _schedule.Add(entry); } From 415c7a6bfd353c92dc3eed0f89e5718e44ca9fd2 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:33:59 -0700 Subject: [PATCH 39/53] fix: Optimizes EventScheduler by using PriorityQueue (#2167) --- .../Engines/Events/EventSchedulerTests.cs | 16 ++-- .../Engines/Events/EventScheduler.cs | 75 ++++++------------- 2 files changed, 30 insertions(+), 61 deletions(-) diff --git a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs index d44116a3b..b9925a0d2 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs @@ -86,7 +86,7 @@ public class EventSchedulerTests Assert.True(called); Assert.Equal(Core._now, evt.NextOccurrence); - EventScheduler.Shared.StopEvent(evt); + evt.Cancel(); } finally { @@ -131,9 +131,9 @@ public class EventSchedulerTests // Verify they executed in time order, not addition order Assert.Equal([1, 2, 3], executionOrder); - EventScheduler.Shared.StopEvent(evt1); - EventScheduler.Shared.StopEvent(evt2); - EventScheduler.Shared.StopEvent(evt3); + evt1.Cancel(); + evt2.Cancel(); + evt3.Cancel(); } finally { @@ -169,7 +169,7 @@ public class EventSchedulerTests Assert.Equal(3, callCount); Assert.Equal(3, evt.CallCount); - EventScheduler.Shared.StopEvent(evt); + evt.Cancel(); } finally { @@ -211,8 +211,8 @@ public class EventSchedulerTests Assert.True(failedEventCalled); Assert.True(laterEventCalled); - EventScheduler.Shared.StopEvent(failedEvt); - EventScheduler.Shared.StopEvent(laterEvt); + failedEvt.Cancel(); + laterEvt.Cancel(); } finally { @@ -235,7 +235,7 @@ public class EventSchedulerTests ); // Remove before execution - EventScheduler.Shared.StopEvent(evt); + evt.Cancel(); // Advance time Core._now = Core._now.AddSeconds(20); diff --git a/Projects/UOContent/Engines/Events/EventScheduler.cs b/Projects/UOContent/Engines/Events/EventScheduler.cs index cf681d33e..6656f829d 100644 --- a/Projects/UOContent/Engines/Events/EventScheduler.cs +++ b/Projects/UOContent/Engines/Events/EventScheduler.cs @@ -33,15 +33,12 @@ public enum DaysOfWeek : byte public abstract class ScheduledEvent { - private static Serial _nextSerial = (Serial)1; - - // Tie-breaker for sorted set - public Serial Serial { get; } public IRecurrencePattern Recurrence { get; } public TimeZoneInfo TimeZone { get; } public TimeOnly Time { get; } public DateTime EndDate { get; } public DateTime NextOccurrence { get; private set; } + public bool Cancelled { get; private set; } public ScheduledEvent(DateTime startOn, TimeZoneInfo timeZone = null) : this(startOn, startOn, TimeOnly.FromDateTime(startOn), null, timeZone) @@ -61,7 +58,6 @@ public abstract class ScheduledEvent TimeZoneInfo timeZone = null ) { - Serial = _nextSerial++; Time = time; Recurrence = recurrence; TimeZone = timeZone ?? TimeZoneInfo.Utc; @@ -71,18 +67,19 @@ public abstract class ScheduledEvent EndDate = endOn == DateTime.MaxValue || endOn.Kind == DateTimeKind.Utc ? endOn : endOn.LocalToUtc(TimeZone); } - public bool Advance() + public void Cancel() => Cancelled = true; + + public DateTime Advance() { OnEvent(); var next = Recurrence?.GetNextOccurrence(NextOccurrence, Time, TimeZone) ?? DateTime.MaxValue; if (next == DateTime.MaxValue || next > EndDate) { - return false; + return DateTime.MaxValue; } - NextOccurrence = next; - return true; + return NextOccurrence = next; } public abstract void OnEvent(); @@ -92,7 +89,7 @@ public class EventScheduler : Timer { private static readonly ILogger logger = LogFactory.GetLogger(typeof(EventScheduler)); - private readonly SortedSet _schedule = new(ScheduledEventComparer.Default); + private readonly PriorityQueue _schedule = new(); public static EventScheduler Shared { get; private set; } @@ -182,15 +179,7 @@ public class EventScheduler : Timer { if (entry != null) { - _schedule.Add(entry); - } - } - - public void StopEvent(ScheduledEvent entry) - { - if (entry != null) - { - _schedule.Remove(entry); + _schedule.Enqueue(entry, entry.NextOccurrence); } } @@ -200,55 +189,35 @@ public class EventScheduler : Timer while (_schedule.Count > 0) { - var entry = _schedule.Min!; - if (entry.NextOccurrence > now) + var entry = _schedule.Peek(); + var cancelled = entry.Cancelled; + if (!cancelled && entry.NextOccurrence > now) { break; } - _schedule.Remove(entry); + _schedule.Dequeue(); - bool advance; + if (cancelled) + { + continue; + } + + DateTime nextOccurrence; try { - advance = entry.Advance(); + nextOccurrence = entry.Advance(); } catch (Exception e) { logger.Error(e, "Error while executing scheduled event."); - advance = false; + nextOccurrence = DateTime.MaxValue; } - if (advance && entry.NextOccurrence < DateTime.MaxValue) + if (nextOccurrence < DateTime.MaxValue) { - _schedule.Add(entry); + _schedule.Enqueue(entry, nextOccurrence); } } } - - private sealed class ScheduledEventComparer : IComparer - { - public static readonly ScheduledEventComparer Default = new(); - - public int Compare(ScheduledEvent x, ScheduledEvent y) - { - if (x == null && y == null) - { - return 0; - } - - if (x == null) - { - return 1; - } - - if (y == null) - { - return -1; - } - - var next = x.NextOccurrence.CompareTo(y.NextOccurrence); - return next != 0 ? next : x.Serial.CompareTo(y.Serial); - } - } } From 7dbfc9d161e72481ad7fd4d37d11ae9cb1c6c6cb Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:42:38 -0700 Subject: [PATCH 40/53] fix: Fixes flaky tests, formatting, and sequential testing. (#2168) --- .../Server.Tests/Fixtures/ServerFixture.cs | 7 +- .../Fixtures/TestMapDefinitions.cs | 53 +- .../SequentialTestCollectionDefinition.cs | 9 - .../Tests/Buffers/STArrayPoolTests.cs | 2 +- .../Tests/Buffers/ValueStringBuilderTests.cs | 2 +- .../Tests/Client/ClientVersionTests.cs | 2 +- .../Tests/Geometry/WorldLocationTests.cs | 3 +- .../Tests/Items/ContainerTests.cs | 3 +- .../Localization/LocalizationEntryTests.cs | 2 +- .../Packets/Outgoing/AccountPacketTests.cs | 3 +- .../Packets/Outgoing/ContainerPacketTests.cs | 4 +- .../Packets/Outgoing/DamagePacketTests.cs | 3 +- .../Packets/Outgoing/EquipmentPacketTests.cs | 3 +- .../Packets/Outgoing/GumpPacketTests.cs | 4 +- .../Packets/Outgoing/ItemPacketTests.cs | 3 +- .../Packets/Outgoing/MapPacketTests.cs | 3 +- .../Packets/Outgoing/MenuPacketTests.cs | 4 +- .../Packets/Outgoing/MobilePacketTests.cs | 4 +- .../Packets/Outgoing/MovementPacketTests.cs | 3 +- .../Packets/Outgoing/PlayerPacketTests.cs | 4 +- .../Outgoing/SecureTradePacketTests.cs | 3 +- .../Packets/Outgoing/VendorBuyPacketTests.cs | 4 +- .../Packets/Outgoing/VendorSellPacketTests.cs | 4 +- .../Outgoing/VirtualHairPacketTests.cs | 52 +- .../Tests/Serialization/TypeConverterTests.cs | 3 +- .../Server.Tests/Tests/Timer/TimerTests.cs | 4 +- .../Tests/Utility/StringHelperTests.cs | 2 +- .../Tests/World/VirtualSerialTests.cs | 4 +- .../{ServerFixture.cs => UOContentFixture.cs} | 15 +- .../Tests/Engines/Chat/ChatPacketTests.cs | 10 +- .../Tests/Engines/Chat/Packets.cs | 32 +- .../Engines/Events/EventSchedulerTests.cs | 51 +- .../Tests/Engines/Help/HelpTopic.cs | 14 +- .../Tests/Engines/Help/TestHelpTopicPacket.cs | 10 +- .../Tests/Engines/ML Quests/Packets.cs | 22 +- .../Tests/Engines/Party/Packets.cs | 72 +-- .../Tests/Engines/Party/PartyPacketTests.cs | 77 +-- .../CharacterStatuePacketTests.cs | 10 +- .../Character Statue Maker/Packets.cs | 26 +- .../Tests/Gumps/TestLayoutGumps.cs | 2 +- .../Tests/Items/Books/BookPacketTests.cs | 83 +-- .../Tests/Items/Books/Packets.cs | 64 +- .../BulletinBoardPacketTests.cs | 40 +- .../Tests/Items/Bulletin Boards/Packets.cs | 273 ++++---- .../Items/Games/Mahjong/MahjongPacketTests.cs | 84 +-- .../Tests/Items/Games/Mahjong/Packets.cs | 264 ++++---- .../Tests/Items/Maps/Packets.cs | 58 +- .../Tests/Items/Maps/TestMapItemPackets.cs | 30 +- .../Items/Misc/Corpses/CorpsePacketTests.cs | 44 +- .../Abilities/WeaponAbilityPacketTests.cs | 18 +- .../Weapons/Abilities/WeaponAbilityPackets.cs | 18 +- .../Tests/Multis/Boats/BoatPacketTests.cs | 3 +- .../Tests/Multis/Boats/Packets.cs | 192 +++--- .../Tests/Multis/Houses/HousePacketTests.cs | 70 +-- .../Tests/Multis/Houses/HousePackets.cs | 591 +++++++++--------- .../Tests/Network/Packets/ArrowPacketTests.cs | 42 +- .../Tests/Network/Packets/ArrowPackets.cs | 38 +- .../Tests/Network/Packets/BuffIconPackets.cs | 137 ++-- .../Tests/Skills/SkillPacketsTests.cs | 3 +- .../UOContent.Tests/UOContent.Tests.csproj | 3 +- 60 files changed, 1304 insertions(+), 1289 deletions(-) delete mode 100644 Projects/Server.Tests/SequentialTestCollectionDefinition.cs rename Projects/UOContent.Tests/Fixtures/{ServerFixture.cs => UOContentFixture.cs} (73%) 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/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/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/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/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/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 index b9925a0d2..5a7718c17 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs @@ -6,7 +6,7 @@ using Xunit; namespace UOContent.Tests; -[Collection("Sequential Tests")] +[Collection("Sequential UOContent Tests")] public class EventSchedulerTests { // Test implementations for controlled testing @@ -144,36 +144,39 @@ public class EventSchedulerTests [Fact] public void Scheduler_HandlesRecurringEvents() { - Init(); - - try + for (var i = 0; i < 100_000; i++) { - int callCount = 0; + Init(); - // Create a recurrence pattern that fires every 10 seconds, up to 3 times - var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3); + try + { + int callCount = 0; - var evt = new TestScheduledEvent( - Core._now, - () => callCount++, - recurrence - ); + // Create a recurrence pattern that fires every 10 seconds, up to 3 times + var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3); - EventScheduler.Shared.ScheduleEvent(evt); + var evt = new TestScheduledEvent( + Core._now, + () => callCount++, + recurrence + ); - // Advance time to after all occurrences should have happened - Core._now = Core._now.AddSeconds(50); - Timer.Slice(8); + EventScheduler.Shared.ScheduleEvent(evt); - // Should have fired 4 times (3 recurrences) - Assert.Equal(3, callCount); - Assert.Equal(3, evt.CallCount); + // Advance time to after all occurrences should have happened + Core._now = Core._now.AddSeconds(50); + Timer.Slice(8); - evt.Cancel(); - } - finally - { - Finish(); + // Should have fired 4 times (3 recurrences) + Assert.Equal(3, callCount); + Assert.Equal(3, evt.CallCount); + + evt.Cancel(); + } + finally + { + Finish(); + } } } 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/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/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 daefd857e..967f997cd 100644 --- a/Projects/UOContent.Tests/UOContent.Tests.csproj +++ b/Projects/UOContent.Tests/UOContent.Tests.csproj @@ -10,7 +10,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + From 1eeeabb7290f316402fb229ee5ebc7c25f623555 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:12:29 -0700 Subject: [PATCH 41/53] fix: Fixes weekly interval schedules for multiple weeks (biweekly) (#2169) --- .../Events/CommonRecurrencePatterns.cs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs b/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs index fb7a11cf4..461f8e38e 100644 --- a/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs +++ b/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs @@ -46,28 +46,29 @@ public class WeeklyRecurrencePattern : IRecurrencePattern public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone) { var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); - var localDate = DateOnly.FromDateTime(local); + var daysOfWeek = DaysOfWeek == DaysOfWeek.None + ? (DaysOfWeek)(1 << (int)local.DayOfWeek) + : DaysOfWeek; - var weekStart = localDate.ToDateTime(time); - var daysOfWeek = DaysOfWeek; + var weekStart = local.Date.AddDays(-(int)local.DayOfWeek); - // Set the day of the week to whatever day it is now - if (daysOfWeek == DaysOfWeek.None) + // No more days in this week, jump IntervalWeeks ahead + for (int week = 0; week <= 100; week++) { - daysOfWeek = (DaysOfWeek)(1 << (int)local.DayOfWeek); - } + DateTime nextWeekStart = weekStart.AddDays(7 * IntervalWeeks * week); - // Example: - // Recurrence is Monday, Wednesday, Friday - and today is Wednesday - // weekStart will be Thursday, and then we check every day for 7 days to find the next occurrence match. - for (int i = 1; i < 8; i++) - { - var candidate = weekStart.AddDays(i); - var candidateDay = (DaysOfWeek)(1 << (int)candidate.DayOfWeek); - - if ((daysOfWeek & candidateDay) != 0 && candidate > local && !timeZone.IsInvalidTime(candidate)) + for (int i = 0; i < 7; i++) { - return candidate.LocalToUtc(timeZone); + var day = nextWeekStart.AddDays(i); + var dayOfWeekFlag = (DaysOfWeek)(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); + } + } } } From 5cb97cc6de7422054e210fd8c757f7aefd6adb13 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 30 Apr 2025 19:33:15 -0700 Subject: [PATCH 42/53] fix: Fixes BitArray serialization (#2170) Fixes serialization/deserialization edge cases with BitArray. If you use BitArray, you will need to migrate. 1. Change the type in the migration JSON file (if there is one) from `BitArray` to `byte[]` for all the versions you need to migrate. 2. Then in the `MigrateFrom`, use the following function to convert the field from a byte[] back to the BitArray. Example Migration JSON: ```json { "name": "RestrictedSpells", "type": "byte[]", "rule": "ArrayMigrationRule", "ruleArguments": [ "byte", "PrimitiveTypeMigrationRule", "" ] }, ``` Migration function to use in MigrateFrom: ```cs public static BitArray MigrateBitArray(byte[] data, int bitLength) => new(data) { Length = bitLength }; ``` Example use: ```cs private void MigrateFrom(V0Content content) { // ... deserialize _restrictedSpells = content.RestrictedSpells.MigrateBitArray(SpellRegistry.Types.Length); _restrictedSkills = content.RestrictedSkills.MigrateBitArray(SkillInfo.Table.Length); // ... rest of deserialize } ``` --- .../Server/Serialization/IGenericReader.cs | 22 ++++++++++++------- .../Server/Serialization/IGenericWriter.cs | 19 +++++++++++----- 2 files changed, 27 insertions(+), 14 deletions(-) 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) From 6d43f2549c4c06805ab290a23b26fb8a00b234be Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 1 May 2025 20:03:56 -0700 Subject: [PATCH 43/53] fix: Adjusts spell ranges to match OSI (#2171) ### Summary Adjusts spell ranges according to OSI. Specifically, the [April 14, 1999 patch](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/1999-2/1999-06-14th-april/) changed spell ranges to 10 tiles, and 15 tiles for fields. --- Projects/UOContent/Spells/Fifth/DispelField.cs | 2 ++ Projects/UOContent/Spells/Fifth/PoisonField.cs | 2 ++ Projects/UOContent/Spells/Fourth/FireField.cs | 2 ++ Projects/UOContent/Spells/Seventh/EnergyField.cs | 2 ++ Projects/UOContent/Spells/Sixth/ParalyzeField.cs | 2 ++ Projects/UOContent/Spells/Targeting/ITargetingSpell.cs | 3 ++- Projects/UOContent/Spells/Third/WallOfStone.cs | 2 ++ 7 files changed, 14 insertions(+), 1 deletion(-) 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/Seventh/EnergyField.cs b/Projects/UOContent/Spells/Seventh/EnergyField.cs index 87dedeaf7..b48bee267 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()) diff --git a/Projects/UOContent/Spells/Sixth/ParalyzeField.cs b/Projects/UOContent/Spells/Sixth/ParalyzeField.cs index ce2ec48f0..14fd088d8 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()) 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()) From da517f56c4cadd4dce1d7784442312ed6879d30b Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 1 May 2025 21:54:41 -0700 Subject: [PATCH 44/53] chore: Fixes variable types for possible performance issues (#2172) --- Projects/Logger/LogFactory.cs | 3 +- .../Server.Tests/Helpers/GumpUtilities.cs | 4 +- Projects/UOContent/Commands/Handlers.cs | 2 +- .../UOContent/Engines/Doom/GenGauntlet.cs | 4 +- .../Doom/LeverPuzzle/LeverPuzzleController.cs | 60 +++++++++---------- .../Doom/LeverPuzzle/LeverPuzzleItems.cs | 2 +- .../ML Quests/Definitions/Heartwood.cs | 52 ++++++++-------- .../Definitions/NewHavenSkillTraining.cs | 18 +++--- .../ML Quests/Definitions/NewHavenTraining.cs | 18 +++--- .../ML Quests/Definitions/Sanctuary.cs | 18 +++--- .../ML Quests/Definitions/Spellweaving.cs | 7 ++- .../Engines/Plants/MiscItems/GreenThorns.cs | 8 +-- .../AmbitiousQueenQuest.cs | 6 +- .../Dark Tides/Items/KronusScrollBox.cs | 2 +- .../Quests/Dark Tides/Mobiles/Mardoth.cs | 14 +++-- .../Items/EminosKatanaChest.cs | 4 +- .../Emino's Undertaking/Mobiles/Emino.cs | 2 +- .../Items/HaochisKatanaGenerator.cs | 4 +- .../Uzeraan Turmoil/Items/DaemonBloodChest.cs | 2 +- .../Quests/Uzeraan Turmoil/Mobiles/Dryad.cs | 4 +- .../Quests/Uzeraan Turmoil/Objectives.cs | 2 +- .../Quests/Witch Apprentice/Objectives.cs | 4 +- .../Treasures of Tokuno/TreasuresOfTokuno.cs | 8 +-- Projects/UOContent/Gumps/VendorRentalGumps.cs | 14 ++--- .../Holiday Stuff/Christmas/2004/Mistletoe.cs | 6 +- .../Halloween/2006/Engines/TrickOrTreat.cs | 2 +- .../UOContent/Items/Containers/Strongbox.cs | 2 +- .../UOContent/Items/Deeds/BarkeepContract.cs | 12 ++-- .../UOContent/Items/Misc/Corpses/Corpse.cs | 2 +- .../UOContent/Items/Misc/PlayerVendorDeed.cs | 12 ++-- .../UOContent/Items/Misc/PublicMoongate.cs | 2 +- .../Items/Resources/Tailor/Cotton.cs | 10 ++-- .../UOContent/Items/Resources/Tailor/Flax.cs | 8 +-- .../UOContent/Items/Resources/Tailor/Wool.cs | 18 +++--- .../Items/Resources/Tailor/YarnsAndThreads.cs | 8 ++- .../Harvest Tools/ProspectorsTool.cs | 2 +- .../UOContent/Items/Special/Holiday/Wreath.cs | 7 ++- .../UOContent/Items/Talismans/BaseTalisman.cs | 3 +- .../UOContent/Items/Traps/FlameSpurtTrap.cs | 3 +- .../UOContent/Items/Weapons/BaseWeapon.cs | 2 +- .../Server.Items.FlameSpurtTrap.v0.json | 2 +- .../Server.Mobiles.LordOaks.v0.json | 2 +- Projects/UOContent/Mobiles/AI/BaseAI.cs | 3 +- .../Mobiles/Animals/Misc/PackHorse.cs | 2 +- Projects/UOContent/Mobiles/BaseCreature.cs | 2 +- .../UOContent/Mobiles/Guards/ArcherGuard.cs | 16 ++--- .../Elemental/Melee/EarthElemental.cs | 7 ++- .../Monsters/Elemental/Melee/SnowElemental.cs | 7 ++- .../Monsters/Humanoid/Melee/OrcBrute.cs | 7 ++- .../Mobiles/Monsters/LBR/Jukas/JukaLord.cs | 2 +- .../Mobiles/Monsters/LBR/Jukas/JukaMage.cs | 2 +- .../Mobiles/Monsters/LBR/Meers/MeerCaptain.cs | 4 +- .../Monsters/ML/Prism of Light/Protector.cs | 22 +++---- .../Monsters/Misc/Melee/PlagueBeast.cs | 12 ++-- .../Ore Elementals/AgapiteElemental.cs | 7 ++- .../Ore Elementals/BronzeElemental.cs | 7 ++- .../Ore Elementals/CopperElemental.cs | 7 ++- .../Ore Elementals/DullCopperElemental.cs | 7 ++- .../Ore Elementals/GoldenElemental.cs | 7 ++- .../Ore Elementals/ShadowIronElemental.cs | 7 ++- .../Ore Elementals/ValoriteElemental.cs | 7 ++- .../Ore Elementals/VeriteElemental.cs | 7 ++- Projects/UOContent/Mobiles/PlayerMobile.cs | 4 +- .../UOContent/Mobiles/Special/Harrower.cs | 2 +- .../UOContent/Mobiles/Special/LordOaks.cs | 2 +- Projects/UOContent/Mobiles/Townfolk/Actor.cs | 8 +-- Projects/UOContent/Mobiles/Townfolk/Artist.cs | 8 +-- .../Mobiles/Townfolk/BaseEscortable.cs | 3 +- Projects/UOContent/Mobiles/Townfolk/Gypsy.cs | 8 +-- .../Mobiles/Townfolk/HarborMaster.cs | 8 +-- .../UOContent/Mobiles/Townfolk/Sculptor.cs | 8 +-- .../UOContent/Mobiles/Vendors/BaseVendor.cs | 4 +- .../Mobiles/Vendors/NPC/Blacksmith.cs | 18 ++---- .../NPC/Guildmasters/BlacksmithGuildmaster.cs | 10 +--- .../UOContent/Mobiles/Vendors/PlayerVendor.cs | 7 ++- Projects/UOContent/Multis/Houses/BaseHouse.cs | 8 +-- .../UOContent/Multis/Houses/MovingCrate.cs | 7 +-- .../Spells/Necromancy/AnimateDeadSpell.cs | 9 +-- .../UOContent/Spells/Seventh/EnergyField.cs | 3 +- .../UOContent/Spells/Sixth/ParalyzeField.cs | 3 +- .../UOContent/Targets/BladedItemTarget.cs | 46 +++++++------- Projects/UOContent/World Saves/AutoArchive.cs | 2 +- 82 files changed, 332 insertions(+), 343 deletions(-) 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/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/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/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/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/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/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/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/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/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 0fd6f4217..c5927a129 100644 --- a/Projects/UOContent/Holiday Stuff/Christmas/2004/Mistletoe.cs +++ b/Projects/UOContent/Holiday Stuff/Christmas/2004/Mistletoe.cs @@ -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); 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/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/Misc/Corpses/Corpse.cs b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs index 4c093f8f0..4c2f1ff26 100644 --- a/Projects/UOContent/Items/Misc/Corpses/Corpse.cs +++ b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs @@ -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/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/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/Holiday/Wreath.cs b/Projects/UOContent/Items/Special/Holiday/Wreath.cs index a715fd1dc..0d0333017 100644 --- a/Projects/UOContent/Items/Special/Holiday/Wreath.cs +++ b/Projects/UOContent/Items/Special/Holiday/Wreath.cs @@ -257,11 +257,12 @@ 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(); } 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 23af56d5f..85489edf3 100644 --- a/Projects/UOContent/Items/Weapons/BaseWeapon.cs +++ b/Projects/UOContent/Items/Weapons/BaseWeapon.cs @@ -3662,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/Mobiles/AI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI.cs index 555c65a46..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)); } } } 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 6dde04f62..ed37b7be1 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -4941,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 91797cc47..6ef01d1ce 100644 --- a/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs +++ b/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs @@ -44,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); 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/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/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index 54c9fe4b0..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(); 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 d95bcf972..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())); 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/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/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/Seventh/EnergyField.cs b/Projects/UOContent/Spells/Seventh/EnergyField.cs index b48bee267..a6fc1b116 100644 --- a/Projects/UOContent/Spells/Seventh/EnergyField.cs +++ b/Projects/UOContent/Spells/Seventh/EnergyField.cs @@ -57,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 14fd088d8..830f16dfe 100644 --- a/Projects/UOContent/Spells/Sixth/ParalyzeField.cs +++ b/Projects/UOContent/Spells/Sixth/ParalyzeField.cs @@ -57,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/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/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()); From f0f7d44d4d467e3ce9bbd4a65229fab6c8771594 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 4 May 2025 09:31:32 -0700 Subject: [PATCH 45/53] fix: Fixes polymorph spell (#2173) --- Projects/UOContent/Gumps/PolymorphGump.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); From 4beda6a29d4ba7f17b1e307b3d7848daf1c5cfb6 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 6 May 2025 21:04:54 -0700 Subject: [PATCH 46/53] fix: Eliminate intermediate string in typecache (#2176) --- Projects/Server/AssemblyHandler.cs | 36 ++++++++++--------- .../Server/Serialization/BinaryFileReader.cs | 2 +- .../Serialization/UnmanagedDataReader.cs | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) 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/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/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
    From f02403a374b5742d0569fc446f9e14cd64e47889 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 6 May 2025 23:52:49 -0700 Subject: [PATCH 47/53] feat: Adds yearly calendared events with recurrences (#2177) --- .../Engines/Events/EventSchedulerTests.cs | 3 +- .../Engines/Events/EventScheduler.cs | 54 -------------- Projects/UOContent/Engines/Events/MonthDay.cs | 66 +++++++++++++++++ .../Engines/Events/ScheduledEvent.cs | 60 ++++++++++++++++ .../Events/YearlyCallbackScheduledEvent.cs | 33 +++++++++ .../Engines/Events/YearlyScheduledEvent.cs | 71 +++++++++++++++++++ 6 files changed, 232 insertions(+), 55 deletions(-) create mode 100644 Projects/UOContent/Engines/Events/MonthDay.cs create mode 100644 Projects/UOContent/Engines/Events/ScheduledEvent.cs create mode 100644 Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs create mode 100644 Projects/UOContent/Engines/Events/YearlyScheduledEvent.cs diff --git a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs index 5a7718c17..22a1ff65e 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs @@ -81,10 +81,11 @@ public class EventSchedulerTests () => called = true ); + Assert.Equal(Core._now, evt.NextOccurrence); Timer.Slice(8); Assert.True(called); - Assert.Equal(Core._now, evt.NextOccurrence); + Assert.Equal(DateTime.MaxValue, evt.NextOccurrence); evt.Cancel(); } diff --git a/Projects/UOContent/Engines/Events/EventScheduler.cs b/Projects/UOContent/Engines/Events/EventScheduler.cs index 6656f829d..8d442fe20 100644 --- a/Projects/UOContent/Engines/Events/EventScheduler.cs +++ b/Projects/UOContent/Engines/Events/EventScheduler.cs @@ -31,60 +31,6 @@ public enum DaysOfWeek : byte EveryDay = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday } -public abstract class ScheduledEvent -{ - public IRecurrencePattern Recurrence { get; } - public TimeZoneInfo TimeZone { get; } - public TimeOnly Time { get; } - public DateTime EndDate { get; } - public DateTime NextOccurrence { get; private set; } - public bool Cancelled { get; private set; } - - public ScheduledEvent(DateTime startOn, TimeZoneInfo timeZone = null) - : this(startOn, startOn, TimeOnly.FromDateTime(startOn), null, timeZone) - { - } - - public ScheduledEvent(DateTime startAfter, TimeOnly time, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null) - : this(startAfter, DateTime.MaxValue, time, recurrence, timeZone) - { - } - - public ScheduledEvent( - DateTime startAfter, - DateTime endOn, - TimeOnly time, - IRecurrencePattern recurrence, - TimeZoneInfo timeZone = null - ) - { - Time = time; - Recurrence = recurrence; - TimeZone = timeZone ?? TimeZoneInfo.Utc; - - var afterUtc = startAfter.Kind == DateTimeKind.Utc ? startAfter : startAfter.LocalToUtc(TimeZone); - NextOccurrence = recurrence?.GetNextOccurrence(afterUtc, time, TimeZone) ?? afterUtc; - EndDate = endOn == DateTime.MaxValue || endOn.Kind == DateTimeKind.Utc ? endOn : endOn.LocalToUtc(TimeZone); - } - - public void Cancel() => Cancelled = true; - - public DateTime Advance() - { - OnEvent(); - - var next = Recurrence?.GetNextOccurrence(NextOccurrence, Time, TimeZone) ?? DateTime.MaxValue; - if (next == DateTime.MaxValue || next > EndDate) - { - return DateTime.MaxValue; - } - - return NextOccurrence = next; - } - - public abstract void OnEvent(); -} - public class EventScheduler : Timer { private static readonly ILogger logger = LogFactory.GetLogger(typeof(EventScheduler)); 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..082017e0e --- /dev/null +++ b/Projects/UOContent/Engines/Events/ScheduledEvent.cs @@ -0,0 +1,60 @@ +using System; + +namespace Server.Engines.Events; + +public abstract class ScheduledEvent +{ + public IRecurrencePattern Recurrence { get; } + public TimeZoneInfo TimeZone { get; } + public TimeOnly Time { get; } + public DateTime EndDate { get; } + public DateTime NextOccurrence { get; protected set; } + public bool Cancelled { get; private set; } + + public ScheduledEvent(DateTime startOn, TimeZoneInfo timeZone = null) + : this(startOn, startOn, TimeOnly.FromDateTime(startOn), null, timeZone) + { + } + + public ScheduledEvent(DateTime startAfter, TimeOnly time, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null) + : this(startAfter, DateTime.MaxValue, time, recurrence, timeZone) + { + } + + public ScheduledEvent( + DateTime startAfter, + DateTime endOn, + TimeOnly time, + IRecurrencePattern recurrence, + TimeZoneInfo timeZone = null + ) + { + Time = time; + Recurrence = recurrence; + TimeZone = timeZone ?? TimeZoneInfo.Utc; + EndDate = endOn == DateTime.MaxValue || endOn.Kind == DateTimeKind.Utc ? endOn : endOn.LocalToUtc(TimeZone); + + var afterUtc = startAfter.Kind == DateTimeKind.Utc ? startAfter : startAfter.LocalToUtc(TimeZone); + + var next = GetOccurrence(afterUtc); + // For the first occurrence, we should set it to the startAfter date if we have no recurrence. + NextOccurrence = next == DateTime.MaxValue ? afterUtc : next; + } + + protected virtual DateTime GetOccurrence(DateTime after) + { + var next = Recurrence?.GetNextOccurrence(after, Time, TimeZone) ?? DateTime.MaxValue; + return next >= EndDate ? DateTime.MaxValue : next; + } + + public void Cancel() => Cancelled = true; + + public virtual DateTime Advance() + { + OnEvent(); + + return NextOccurrence = GetOccurrence(NextOccurrence); + } + + public abstract void OnEvent(); +} diff --git a/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs b/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs new file mode 100644 index 000000000..75036d226 --- /dev/null +++ b/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs @@ -0,0 +1,33 @@ +using System; + +namespace Server.Engines.Events; + +public class YearlyCallbackScheduledEvent : YearlyScheduledEvent +{ + private readonly Action _callback; + + protected YearlyCallbackScheduledEvent( + DateTime startAfter, + TimeOnly time, + MonthDay yearlyStart, + MonthDay yearlyEnd, + Action callback, + IRecurrencePattern recurrence, + TimeZoneInfo timeZone = null + ) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, callback, recurrence, timeZone) + { + } + + protected YearlyCallbackScheduledEvent( + DateTime startAfter, + DateTime endOn, + TimeOnly time, + MonthDay yearlyStart, + MonthDay yearlyEnd, + Action callback, + IRecurrencePattern recurrence, + TimeZoneInfo timeZone = null + ) : base(startAfter, endOn, time, yearlyStart, yearlyEnd, recurrence, timeZone) => _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..d29356839 --- /dev/null +++ b/Projects/UOContent/Engines/Events/YearlyScheduledEvent.cs @@ -0,0 +1,71 @@ +using System; + +namespace Server.Engines.Events; + +public abstract class YearlyScheduledEvent : ScheduledEvent +{ + public MonthDay YearlyStart { get; } + + public MonthDay YearlyEnd { get; } + + protected YearlyScheduledEvent( + DateTime startAfter, + TimeOnly time, + MonthDay yearlyStart, + MonthDay yearlyEnd, + IRecurrencePattern recurrence, + TimeZoneInfo timeZone = null + ) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, recurrence, timeZone) + { + } + + protected YearlyScheduledEvent( + DateTime startAfter, + DateTime endOn, + TimeOnly time, + MonthDay yearlyStart, + MonthDay yearlyEnd, + IRecurrencePattern recurrence, + TimeZoneInfo timeZone = null + ) : base(startAfter, endOn, time, recurrence, timeZone) + { + YearlyStart = yearlyStart; + YearlyEnd = yearlyEnd; + } + + protected override DateTime GetOccurrence(DateTime after) + { + var next = base.GetOccurrence(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); + } +} From 990e86b983c3a7d2bbc4ef9c992bd0781537f940 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 6 May 2025 23:54:13 -0700 Subject: [PATCH 48/53] fix: Fixes guard infinite loop, and streamlines call methods (#2175) --- .../UOContent/Mobiles/Guards/ArcherGuard.cs | 2 +- .../UOContent/Mobiles/Guards/BaseGuard.cs | 35 ++++++++----------- .../UOContent/Mobiles/Guards/WarriorGuard.cs | 2 +- Projects/UOContent/Regions/GuardedRegion.cs | 31 ++++------------ 4 files changed, 24 insertions(+), 46 deletions(-) diff --git a/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs b/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs index 6ef01d1ce..895e0724f 100644 --- a/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs +++ b/Projects/UOContent/Mobiles/Guards/ArcherGuard.cs @@ -108,7 +108,7 @@ public partial class ArcherGuard : BaseGuard if (_focus != null) { AttackTimer = new GuardAttackTimer(this); - AttackTimer.DoOnTick(); + AttackTimer.Start(); } else { diff --git a/Projects/UOContent/Mobiles/Guards/BaseGuard.cs b/Projects/UOContent/Mobiles/Guards/BaseGuard.cs index 571b16321..6dd80edb1 100644 --- a/Projects/UOContent/Mobiles/Guards/BaseGuard.cs +++ b/Projects/UOContent/Mobiles/Guards/BaseGuard.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using ModernUO.Serialization; using Server.Items; @@ -13,33 +14,34 @@ public abstract partial class BaseGuard : Mobile GuardsInstantKill = ServerConfiguration.GetSetting("guards.instantKill", true); } - public static void Spawn(Mobile caller, Mobile target, int amount = 1, bool onlyAdditional = false) + [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 m in target.GetMobilesInRange(15)) + foreach (var g in target.GetMobilesInRange(15)) { - if (m is BaseGuard g) + if (g.Focus == null) // idling { - if (g.Focus == null) // idling - { - g.Focus = target; + g.Focus = target; - --amount; - } - else if (g.Focus == target && !onlyAdditional) - { - --amount; - } + --amount; + } + else if (g.Focus == target && !onlyAdditional) + { + --amount; } } while (amount-- > 0) { - caller.Region.MakeGuard(target); + region.MakeGuard(target); } } @@ -242,13 +244,6 @@ public class GuardAttackTimer : Timer return; } - if (_owner.Weapon is Fists) - { - _owner.Kill(); - Stop(); - return; - } - if (target != null && _owner.Combatant != target) { _owner.Combatant = target; diff --git a/Projects/UOContent/Mobiles/Guards/WarriorGuard.cs b/Projects/UOContent/Mobiles/Guards/WarriorGuard.cs index 3ee7bf47f..c402980b4 100644 --- a/Projects/UOContent/Mobiles/Guards/WarriorGuard.cs +++ b/Projects/UOContent/Mobiles/Guards/WarriorGuard.cs @@ -124,7 +124,7 @@ public partial class WarriorGuard : BaseGuard if (_focus != null) { AttackTimer = new GuardAttackTimer(this); - AttackTimer.DoOnTick(); + AttackTimer.Start(); } else { diff --git a/Projects/UOContent/Regions/GuardedRegion.cs b/Projects/UOContent/Regions/GuardedRegion.cs index 9b48caf2d..bfd54bd03 100644 --- a/Projects/UOContent/Regions/GuardedRegion.cs +++ b/Projects/UOContent/Regions/GuardedRegion.cs @@ -152,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 } } @@ -296,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. @@ -332,7 +315,7 @@ public class GuardedRegion : BaseRegion while (queue.Count > 0) { var m = queue.Dequeue(); - MakeGuard(m); + BaseGuard.Spawn(this, m); m.SendLocalizedMessage(502276); // Guards can no longer be called on you. } } From f95b3a8b3b7f0ac1a0706491efc8bb30fc67b5db Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 7 May 2025 22:42:44 -0700 Subject: [PATCH 49/53] fix: Streamlines event scheduler API. Adds months to weekly recurrence (#2178) --- .../Engines/Events/EventSchedulerTests.cs | 41 ++--- .../Engines/Events/RecurrencePatternTests.cs | 147 +++++++++++++++++- .../UOContent/Engines/Events/AllowedDays.cs | 22 +++ .../UOContent/Engines/Events/AllowedMonths.cs | 23 +++ .../Engines/Events/BaseScheduledEvent.cs | 67 ++++++++ .../Engines/Events/CallbackScheduledEvent.cs | 16 +- .../Events/CommonRecurrencePatterns.cs | 45 ++++-- .../Engines/Events/EventScheduler.cs | 51 ++---- .../Engines/Events/ScheduledEvent.cs | 40 +---- .../Events/YearlyCallbackScheduledEvent.cs | 14 +- .../Engines/Events/YearlyScheduledEvent.cs | 18 +-- 11 files changed, 347 insertions(+), 137 deletions(-) create mode 100644 Projects/UOContent/Engines/Events/AllowedDays.cs create mode 100644 Projects/UOContent/Engines/Events/AllowedMonths.cs create mode 100644 Projects/UOContent/Engines/Events/BaseScheduledEvent.cs diff --git a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs index 22a1ff65e..65da0271c 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs @@ -41,8 +41,8 @@ public class EventSchedulerTests public int CallCount { get; private set; } public Action Callback { get; } - public TestScheduledEvent(DateTime startAfter, Action callback, IRecurrencePattern recurrence = null) - : base(startAfter, TimeOnly.FromDateTime(startAfter), recurrence, TimeZoneInfo.Utc) + public TestScheduledEvent(TimeOnly time, Action callback, IRecurrencePattern recurrence = null) + : base(time, recurrence) { CallCount = 0; Callback = callback; @@ -105,25 +105,28 @@ public class EventSchedulerTests var executionOrder = new List(); // Create events with staggered occurrences - var evt3 = new TestScheduledEvent( - Core._now.AddSeconds(30), - () => executionOrder.Add(3) - ); - + var evt1Start = Core._now.AddSeconds(10); var evt1 = new TestScheduledEvent( - Core._now.AddSeconds(10), + TimeOnly.FromDateTime(Core._now.AddSeconds(10)), () => executionOrder.Add(1) ); + var evt2Start = Core._now.AddSeconds(20); var evt2 = new TestScheduledEvent( - Core._now.AddSeconds(20), + 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 - EventScheduler.Shared.ScheduleEvent(evt3); - EventScheduler.Shared.ScheduleEvent(evt1); - EventScheduler.Shared.ScheduleEvent(evt2); + evt3.Schedule(evt3Start); + evt1.Schedule(evt1Start); + evt2.Schedule(evt2Start); // Advance time to after all events Core._now = Core._now.AddSeconds(40); @@ -157,12 +160,12 @@ public class EventSchedulerTests var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3); var evt = new TestScheduledEvent( - Core._now, + TimeOnly.FromDateTime(Core._now), () => callCount++, recurrence ); - EventScheduler.Shared.ScheduleEvent(evt); + evt.Schedule(Core._now); // Advance time to after all occurrences should have happened Core._now = Core._now.AddSeconds(50); @@ -271,14 +274,13 @@ public class EventSchedulerTests var endTime = Core._now.AddSeconds(36); var customEvent = new CustomEvent( - startTime, - endTime, TimeOnly.FromDateTime(startTime), + endTime, recurrence, () => callCount++ ); - EventScheduler.Shared.ScheduleEvent(customEvent); + customEvent.Schedule(startTime); Core._now = Core._now.AddSeconds(50); Timer.Slice(8); @@ -297,12 +299,11 @@ public class EventSchedulerTests private readonly Action _callback; public CustomEvent( - DateTime startAfter, - DateTime endOn, TimeOnly time, + DateTime endOn, IRecurrencePattern recurrence, Action callback - ) : base(startAfter, endOn, time, recurrence, TimeZoneInfo.Utc) => _callback = callback; + ) : base(time, endOn, recurrence) => _callback = callback; public override void OnEvent() { diff --git a/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs b/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs index 7917194c8..b08a49007 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs @@ -51,6 +51,152 @@ public class RecurrencePatternTests 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)] @@ -145,7 +291,6 @@ public class RecurrencePatternTests Assert.Equal(new DateTime(2024, 3, 31, 12, 0, 0), result); } - // Additional utility method tests [Theory] [InlineData("America/New_York", 2024, 11, 3, 1, 30)] // Fall back - ambiguous time public void LocalToUtc_AmbiguousTime_HandlesConsistently( 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/CallbackScheduledEvent.cs b/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs index e142cf247..cab4e08fe 100644 --- a/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs +++ b/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs @@ -7,23 +7,17 @@ public sealed class CallbackScheduledEvent : ScheduledEvent private readonly Action _callback; public CallbackScheduledEvent( - DateTime after, TimeOnly time, Action callback, - IRecurrencePattern recurrencePattern = null, - TimeZoneInfo timeZone = null - ) : base(after, time, recurrencePattern, timeZone) => - _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + IRecurrencePattern recurrencePattern = null + ) : base(time, recurrencePattern) => _callback = callback ?? throw new ArgumentNullException(nameof(callback)); public CallbackScheduledEvent( - DateTime after, - DateTime endOn, TimeOnly time, + DateTime endOn, Action callback, - IRecurrencePattern recurrencePattern = null, - TimeZoneInfo timeZone = null - ) : base(after, endOn, time, recurrencePattern, timeZone) => - _callback = callback ?? throw new ArgumentNullException(nameof(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 index 461f8e38e..f60358f3f 100644 --- a/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs +++ b/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs @@ -35,32 +35,49 @@ public class DailyRecurrencePattern : IRecurrencePattern public class WeeklyRecurrencePattern : IRecurrencePattern { public int IntervalWeeks { get; } - public DaysOfWeek DaysOfWeek { get; } + public AllowedDays AllowedDays { get; } + public AllowedMonths AllowedMonths { get; } - public WeeklyRecurrencePattern(int intervalWeeks = 1, DaysOfWeek daysOfWeek = DaysOfWeek.None) + public WeeklyRecurrencePattern(int intervalWeeks = 1, AllowedMonths allowedMonths = AllowedMonths.All, AllowedDays allowedDays = AllowedDays.None) { IntervalWeeks = Math.Max(1, intervalWeeks); - DaysOfWeek = daysOfWeek; + 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 = DaysOfWeek == DaysOfWeek.None - ? (DaysOfWeek)(1 << (int)local.DayOfWeek) - : DaysOfWeek; + 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 (int week = 0; week <= 100; week++) + for (var week = 0; week <= 52; week++) { - DateTime nextWeekStart = weekStart.AddDays(7 * IntervalWeeks * week); + var nextWeekStart = weekStart.AddDays(7 * IntervalWeeks * week); + var weekEnd = nextWeekStart.AddDays(6); - for (int i = 0; i < 7; i++) + 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 dayOfWeekFlag = (DaysOfWeek)(1 << (int)day.DayOfWeek); + + 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); @@ -94,7 +111,7 @@ public class MonthlyRecurrencePattern : IRecurrencePattern var month = local.Month; var day = DayOfMonth == -1 ? local.Day : DayOfMonth; - for (int i = 0; i < 100; i++) + for (var i = 0; i < 100; i++) { var nextMonth = month + IntervalMonths * i; var candidate = new DateTime(year, 1, 1) @@ -132,7 +149,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern var year = local.Year; var month = local.Month; - for (int i = 0; i < 100; i++) + for (var i = 0; i < 100; i++) { var nextMonth = month + IntervalMonths * i; var candidateYearOffset = Math.DivRem(nextMonth - 1, 12, out var candidateMonthOffset); @@ -147,7 +164,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern .Add(time.ToTimeSpan()); // Find the first occurrence of the desired day - int daysOffset = ((int)DayOfWeek - (int)firstOfMonth.DayOfWeek + 7) % 7; + var daysOffset = ((int)DayOfWeek - (int)firstOfMonth.DayOfWeek + 7) % 7; if (daysOffset > 7) { daysOffset -= 7; @@ -168,7 +185,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern .Add(time.ToTimeSpan()); // Find the last occurrence of the desired day - int daysOffset = (int)lastOfMonth.DayOfWeek - (int)DayOfWeek + 7; + var daysOffset = (int)lastOfMonth.DayOfWeek - (int)DayOfWeek + 7; if (daysOffset >= 7) { daysOffset -= 7; diff --git a/Projects/UOContent/Engines/Events/EventScheduler.cs b/Projects/UOContent/Engines/Events/EventScheduler.cs index 8d442fe20..992110371 100644 --- a/Projects/UOContent/Engines/Events/EventScheduler.cs +++ b/Projects/UOContent/Engines/Events/EventScheduler.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Server.Logging; namespace Server.Engines.Events; @@ -17,25 +16,9 @@ public interface IRecurrencePattern public enum OrdinalDayOccurrence { Last = -1, First, Second, Third, Fourth, Fifth } -[Flags] -public enum DaysOfWeek : byte -{ - None = 0, - Sunday = 1, - Monday = 2, - Tuesday = 4, - Wednesday = 8, - Thursday = 16, - Friday = 32, - Saturday = 64, - EveryDay = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday -} - public class EventScheduler : Timer { - private static readonly ILogger logger = LogFactory.GetLogger(typeof(EventScheduler)); - - private readonly PriorityQueue _schedule = new(); + private readonly PriorityQueue _schedule = new(); public static EventScheduler Shared { get; private set; } @@ -116,16 +99,24 @@ public class EventScheduler : Timer TimeZoneInfo timeZone = null ) { - var scheduledEvent = new CallbackScheduledEvent(after, time, callback, recurrencePattern, timeZone); - ScheduleEvent(scheduledEvent); + var scheduledEvent = new CallbackScheduledEvent(time, callback, recurrencePattern); + scheduledEvent.Schedule(this, after, timeZone); return scheduledEvent; } - public void ScheduleEvent(ScheduledEvent entry) + public void ScheduleEvent(BaseScheduledEvent entry) + { + if (entry != null && entry.NextOccurrence < DateTime.MaxValue) + { + _schedule.Enqueue(entry, entry.NextOccurrence); + } + } + + public void UnscheduleEvent(BaseScheduledEvent entry) { if (entry != null) { - _schedule.Enqueue(entry, entry.NextOccurrence); + _schedule.Remove(entry, out _, out _); } } @@ -149,21 +140,7 @@ public class EventScheduler : Timer continue; } - DateTime nextOccurrence; - try - { - nextOccurrence = entry.Advance(); - } - catch (Exception e) - { - logger.Error(e, "Error while executing scheduled event."); - nextOccurrence = DateTime.MaxValue; - } - - if (nextOccurrence < DateTime.MaxValue) - { - _schedule.Enqueue(entry, nextOccurrence); - } + entry.Advance(); // Advances the event and self queues if necessary } } } diff --git a/Projects/UOContent/Engines/Events/ScheduledEvent.cs b/Projects/UOContent/Engines/Events/ScheduledEvent.cs index 082017e0e..3f90227cb 100644 --- a/Projects/UOContent/Engines/Events/ScheduledEvent.cs +++ b/Projects/UOContent/Engines/Events/ScheduledEvent.cs @@ -2,59 +2,31 @@ using System; namespace Server.Engines.Events; -public abstract class ScheduledEvent +public abstract class ScheduledEvent : BaseScheduledEvent { public IRecurrencePattern Recurrence { get; } - public TimeZoneInfo TimeZone { get; } public TimeOnly Time { get; } public DateTime EndDate { get; } - public DateTime NextOccurrence { get; protected set; } - public bool Cancelled { get; private set; } - public ScheduledEvent(DateTime startOn, TimeZoneInfo timeZone = null) - : this(startOn, startOn, TimeOnly.FromDateTime(startOn), null, timeZone) - { - } - - public ScheduledEvent(DateTime startAfter, TimeOnly time, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null) - : this(startAfter, DateTime.MaxValue, time, recurrence, timeZone) + public ScheduledEvent(TimeOnly time, IRecurrencePattern recurrence = null) + : this(time, DateTime.MaxValue, recurrence) { } public ScheduledEvent( - DateTime startAfter, - DateTime endOn, TimeOnly time, - IRecurrencePattern recurrence, - TimeZoneInfo timeZone = null + DateTime endOn, + IRecurrencePattern recurrence = null ) { Time = time; Recurrence = recurrence; - TimeZone = timeZone ?? TimeZoneInfo.Utc; EndDate = endOn == DateTime.MaxValue || endOn.Kind == DateTimeKind.Utc ? endOn : endOn.LocalToUtc(TimeZone); - - var afterUtc = startAfter.Kind == DateTimeKind.Utc ? startAfter : startAfter.LocalToUtc(TimeZone); - - var next = GetOccurrence(afterUtc); - // For the first occurrence, we should set it to the startAfter date if we have no recurrence. - NextOccurrence = next == DateTime.MaxValue ? afterUtc : next; } - protected virtual DateTime GetOccurrence(DateTime after) + protected override DateTime GetNextOccurrence(DateTime after) { var next = Recurrence?.GetNextOccurrence(after, Time, TimeZone) ?? DateTime.MaxValue; return next >= EndDate ? DateTime.MaxValue : next; } - - public void Cancel() => Cancelled = true; - - public virtual DateTime Advance() - { - OnEvent(); - - return NextOccurrence = GetOccurrence(NextOccurrence); - } - - public abstract void OnEvent(); } diff --git a/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs b/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs index 75036d226..f4ca34f65 100644 --- a/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs +++ b/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs @@ -7,27 +7,23 @@ public class YearlyCallbackScheduledEvent : YearlyScheduledEvent private readonly Action _callback; protected YearlyCallbackScheduledEvent( - DateTime startAfter, TimeOnly time, MonthDay yearlyStart, MonthDay yearlyEnd, Action callback, - IRecurrencePattern recurrence, - TimeZoneInfo timeZone = null - ) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, callback, recurrence, timeZone) + IRecurrencePattern recurrence + ) : this(time, yearlyStart, yearlyEnd, DateTime.MaxValue, callback, recurrence) { } protected YearlyCallbackScheduledEvent( - DateTime startAfter, - DateTime endOn, TimeOnly time, MonthDay yearlyStart, MonthDay yearlyEnd, + DateTime endOn, Action callback, - IRecurrencePattern recurrence, - TimeZoneInfo timeZone = null - ) : base(startAfter, endOn, time, yearlyStart, yearlyEnd, recurrence, timeZone) => _callback = 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 index d29356839..0deceaca8 100644 --- a/Projects/UOContent/Engines/Events/YearlyScheduledEvent.cs +++ b/Projects/UOContent/Engines/Events/YearlyScheduledEvent.cs @@ -9,33 +9,29 @@ public abstract class YearlyScheduledEvent : ScheduledEvent public MonthDay YearlyEnd { get; } protected YearlyScheduledEvent( - DateTime startAfter, TimeOnly time, MonthDay yearlyStart, MonthDay yearlyEnd, - IRecurrencePattern recurrence, - TimeZoneInfo timeZone = null - ) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, recurrence, timeZone) + IRecurrencePattern recurrence + ) : this( time, yearlyStart, yearlyEnd, DateTime.MaxValue, recurrence) { } protected YearlyScheduledEvent( - DateTime startAfter, - DateTime endOn, TimeOnly time, MonthDay yearlyStart, MonthDay yearlyEnd, - IRecurrencePattern recurrence, - TimeZoneInfo timeZone = null - ) : base(startAfter, endOn, time, recurrence, timeZone) + DateTime endOn, + IRecurrencePattern recurrence + ) : base(time, endOn, recurrence) { YearlyStart = yearlyStart; YearlyEnd = yearlyEnd; } - protected override DateTime GetOccurrence(DateTime after) + protected override DateTime GetNextOccurrence(DateTime after) { - var next = base.GetOccurrence(after); + var next = base.GetNextOccurrence(after); if (next == DateTime.MaxValue) { From 05825a11e223ee67df08ee525da003d26f3e3529 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 9 May 2025 15:39:47 -0700 Subject: [PATCH 50/53] fix: Fixes props for interfaces and adds account manipulation (#2179) --- Projects/Server/Attributes.cs | 20 ++++++++++++-------- Projects/Server/Mobiles/Mobile.cs | 2 +- Projects/UOContent/Accounting/Account.cs | 1 + 3 files changed, 14 insertions(+), 9 deletions(-) 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/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index a3cd7620d..5b4e7a992 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -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)] 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 { From 18903ee17b4a6c3651f12e4b1919b3c427492466 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 9 May 2025 15:44:32 -0700 Subject: [PATCH 51/53] fix: Fixes props gump for interfaces again (#2180) --- Projects/UOContent/Gumps/Props/PropsGump.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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)) From 0f3d984efc23724c6997436e9a8469fdb2806fe2 Mon Sep 17 00:00:00 2001 From: mdodkins Date: Mon, 12 May 2025 02:13:00 +0100 Subject: [PATCH 52/53] fix: Fixes craft gump non-cliloc label offsets. (#2182) --- .../UOContent/Engines/Craft/Core/CraftGump.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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); } } } From f2526c82f38090d4613f4e557790020002e330ed Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Mon, 12 May 2025 12:24:24 -0700 Subject: [PATCH 53/53] fix: Fixes Freeshard Protocol (UOGateway) support. (#2183) --- Projects/Server/Network/TcpServer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index 20c97f792..5e5fe3a1b 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -14,6 +14,7 @@ *************************************************************************/ using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.Linq; using System.Net; @@ -173,9 +174,15 @@ public static class TcpServer 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)