diff --git a/.editorconfig b/.editorconfig index 6cf20e3167..5acefbd372 100644 --- a/.editorconfig +++ b/.editorconfig @@ -135,6 +135,19 @@ dotnet_style_qualification_for_field=false:suggestion dotnet_style_qualification_for_method=false:suggestion dotnet_style_qualification_for_property=false:suggestion dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion +# Added by Derek +dotnet_diagnostic.IDE0022.severity = none # expression bodies +dotnet_diagnostic.IDE0130.severity = none # namespace not matching file location +dotnet_diagnostic.IDE0290.severity = none # primary constructors +dotnet_diagnostic.IDE1006.severity = none # naming violations with "_" +dotnet_diagnostic.IDE0038.severity = none # pattern matching +dotnet_diagnostic.IDE0008.severity = none # using var +# probably should be ruled var or not var across the board but it's just suggestions anyway. +# Leave this here in case we want to turn it on in the future +# csharp_style_var_for_built_in_types = true:suggestion # suggest using var (implied) variables +# csharp_style_var_when_type_is_apparent = true:suggestion # suggest using var (implied) variables +# csharp_style_var_elsewhere = true:suggestion # suggest using var (implied) variables +dotnet_diagnostic.RCS1123.severity = none # ReSharper properties resharper_apply_auto_detected_rules=false diff --git a/Directory.Build.props b/Directory.Build.props index 7aac5ce87b..68c23b16b0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -62,11 +62,11 @@ latest - + - 3.6.139 + 3.6.143 all diff --git a/Distribution/Data/Decoration/Felucca/ocllo.cfg b/Distribution/Data/Decoration/Felucca/ocllo.cfg index b4f25611f7..a434e69f6f 100644 --- a/Distribution/Data/Decoration/Felucca/ocllo.cfg +++ b/Distribution/Data/Decoration/Felucca/ocllo.cfg @@ -1060,7 +1060,7 @@ TreatiseOnAlchemy 0x0FF4 3672 2475 4 # spinning wheel -Static 0x1019 +SpinningWheelEastAddon 0x1019 3667 2597 0 # pile of wool diff --git a/Projects/Application/Application.cs b/Projects/Application/Application.cs index 28c3aeda16..6eb5d213bb 100644 --- a/Projects/Application/Application.cs +++ b/Projects/Application/Application.cs @@ -7,16 +7,6 @@ public class Application { public static void Main(string[] args) { - bool profiling = false; - - foreach (var a in args) - { - if (a.InsensitiveEquals("-profile")) - { - profiling = true; - } - } - - Core.Setup(profiling, Assembly.GetEntryAssembly(), Process.GetCurrentProcess()); + Core.Setup(Assembly.GetEntryAssembly(), Process.GetCurrentProcess()); } } diff --git a/Projects/Server.Tests/Helpers/Packet.cs b/Projects/Server.Tests/Helpers/Packet.cs index 9432fc44eb..e9d1d421c2 100644 --- a/Projects/Server.Tests/Helpers/Packet.cs +++ b/Projects/Server.Tests/Helpers/Packet.cs @@ -2,7 +2,6 @@ using System.Buffers; using System.Diagnostics; using System.IO; -using Server.Diagnostics; namespace Server.Network { @@ -19,12 +18,6 @@ public abstract class Packet protected Packet(int packetID) { PacketID = packetID; - - if (Core.Profiling) - { - var prof = PacketSendProfile.Acquire(PacketID); - prof.Increment(); - } } protected Packet(int packetID, int length) @@ -34,12 +27,6 @@ protected Packet(int packetID, int length) Stream = PacketWriter.CreateInstance(length); // new PacketWriter( length ); Stream.Write((byte)packetID); - - if (Core.Profiling) - { - var prof = PacketSendProfile.Acquire(PacketID); - prof.Increment(); - } } public int PacketID { get; } diff --git a/Projects/Server.Tests/Server.Tests.csproj b/Projects/Server.Tests/Server.Tests.csproj index 46715636ec..a798866ec6 100644 --- a/Projects/Server.Tests/Server.Tests.csproj +++ b/Projects/Server.Tests/Server.Tests.csproj @@ -5,8 +5,8 @@ Server.Tests - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs index 0ad93a512f..ed3bfd4e35 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs @@ -51,6 +51,10 @@ public Mobile this[int index] public Serial Serial { get; } public void Deserialize(IGenericReader reader) => throw new NotImplementedException(); + public byte SerializedThread { get; set; } + public int SerializedPosition { get; set; } + public int SerializedLength { get; set; } + public void Serialize(IGenericWriter writer) => throw new NotImplementedException(); public bool Deleted { get; } diff --git a/Projects/Server/Buffers/STArrayPool.cs b/Projects/Server/Buffers/STArrayPool.cs index d53d650ee4..25dd6a7f47 100644 --- a/Projects/Server/Buffers/STArrayPool.cs +++ b/Projects/Server/Buffers/STArrayPool.cs @@ -15,6 +15,16 @@ namespace Server.Buffers; */ public class STArrayPool : ArrayPool { +#if DEBUG_ARRAYPOOL + private class RentReturnStatus + { + public string StackTrace { get; set; } + public bool IsRented { get; set; } + } + + private static readonly ConditionalWeakTable _rentedArrays = new(); +#endif + private const int StackArraySize = 32; private const int BucketCount = 27; // SelectBucketIndex(1024 * 1024 * 1024 + 1) private static readonly STArrayPool _shared = new(); @@ -39,6 +49,12 @@ public override T[] Rent(int minimumLength) if (buffer is not null) { cachedBuckets[bucketIndex].Array = null; +#if DEBUG_ARRAYPOOL + _rentedArrays.AddOrUpdate( + buffer, + new RentReturnStatus { IsRented = true } + ); +#endif return buffer; } } @@ -52,6 +68,12 @@ public override T[] Rent(int minimumLength) buffer = b.TryPop(); if (buffer is not null) { +#if DEBUG_ARRAYPOOL + _rentedArrays.AddOrUpdate( + buffer, + new RentReturnStatus { IsRented = true } + ); +#endif return buffer; } } @@ -70,8 +92,16 @@ public override T[] Rent(int minimumLength) throw new ArgumentOutOfRangeException(nameof(minimumLength)); } - buffer = GC.AllocateUninitializedArray(minimumLength); - return buffer; + var array = GC.AllocateUninitializedArray(minimumLength); + +#if DEBUG_ARRAYPOOL + _rentedArrays.AddOrUpdate( + array, + new RentReturnStatus { IsRented = true, StackTrace = Environment.StackTrace } + ); +#endif + + return array; } public override void Return(T[]? array, bool clearArray = false) @@ -91,10 +121,26 @@ public override void Return(T[]? array, bool clearArray = false) Array.Clear(array); } +#if DEBUG_ARRAYPOOL + if (array.Length != GetMaxSizeForBucket(bucketIndex) || !_rentedArrays.TryGetValue(array, out var status)) + { + throw new ArgumentException("Buffer is not from the pool", nameof(array)); + } + + if (!status!.IsRented) + { + throw new InvalidOperationException($"Array has already been returned.\nOriginal StackTrace:{status.StackTrace}\n"); + } + + // Mark it as returned + status.IsRented = false; + status.StackTrace = Environment.StackTrace; +#else if (array.Length != GetMaxSizeForBucket(bucketIndex)) { throw new ArgumentException("Buffer is not from the pool", nameof(array)); } +#endif ref var bucketArray = ref cacheBuckets[bucketIndex]; var prev = bucketArray.Array; diff --git a/Projects/Server/Diagnostics/BaseProfile.cs b/Projects/Server/Diagnostics/BaseProfile.cs deleted file mode 100644 index 32e20ba556..0000000000 --- a/Projects/Server/Diagnostics/BaseProfile.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; - -namespace Server.Diagnostics; - -public abstract class BaseProfile -{ - private readonly Stopwatch _stopwatch; - - protected BaseProfile(string name) - { - Name = name; - - _stopwatch = new Stopwatch(); - } - - public string Name { get; } - - public long Count { get; private set; } - - public TimeSpan AverageTime => TimeSpan.FromTicks(TotalTime.Ticks / Math.Max(Count, 1)); - - public TimeSpan PeakTime { get; private set; } - - public TimeSpan TotalTime { get; private set; } - - public static void WriteAll(TextWriter op, IEnumerable profiles) where T : BaseProfile - { - var list = new List(profiles); - - list.Sort((a, b) => -a.TotalTime.CompareTo(b.TotalTime)); - - foreach (var prof in list) - { - prof.WriteTo(op); - op.WriteLine(); - } - } - - public virtual void Start() - { - if (_stopwatch.IsRunning) - { - _stopwatch.Reset(); - } - - _stopwatch.Start(); - } - - public virtual void Finish() - { - var elapsed = _stopwatch.Elapsed; - - TotalTime += elapsed; - - if (elapsed > PeakTime) - { - PeakTime = elapsed; - } - - Count++; - - _stopwatch.Reset(); - } - - public virtual void WriteTo(TextWriter op) - { - op.Write( - "{0,-100} {1,12:N0} {2,12:F5} {3,-12:F5} {4,12:F5}", - Name, - Count, - AverageTime.TotalSeconds, - PeakTime.TotalSeconds, - TotalTime.TotalSeconds - ); - } -} diff --git a/Projects/Server/Diagnostics/PacketProfile.cs b/Projects/Server/Diagnostics/PacketProfile.cs deleted file mode 100644 index 2a731233c2..0000000000 --- a/Projects/Server/Diagnostics/PacketProfile.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; - -namespace Server.Diagnostics; - -public abstract class BasePacketProfile : BaseProfile -{ - protected BasePacketProfile(string name) : base(name) - { - } - - public long TotalLength { get; private set; } - - public double AverageLength => (double)TotalLength / Math.Max(Count, 1); - - public void Finish(long length) - { - Finish(); - - TotalLength += length; - } - - public override void WriteTo(TextWriter op) - { - base.WriteTo(op); - - op.Write("\t{0,12:F2} {1,-12:N0}", AverageLength, TotalLength); - } -} - -public class PacketSendProfile : BasePacketProfile -{ - private static readonly Dictionary _profiles = new(); - - private long _created; - - public PacketSendProfile(int packetId) : base($"0x{packetId:X2}") - { - } - - public static IEnumerable Profiles => _profiles.Values; - - public static PacketSendProfile Acquire(int packetId) - { - if (!_profiles.TryGetValue(packetId, out var prof)) - { - _profiles.Add(packetId, prof = new PacketSendProfile(packetId)); - } - - return prof; - } - - public void Increment() - { - Interlocked.Increment(ref _created); - } - - public override void WriteTo(TextWriter op) - { - base.WriteTo(op); - - op.Write("\t{0,12:N0}", _created); - } -} - -public class PacketReceiveProfile : BasePacketProfile -{ - private static readonly Dictionary - _profiles = new(); - - public PacketReceiveProfile(int packetId) : base($"0x{packetId:X2}") - { - } - - public static IEnumerable Profiles => _profiles.Values; - - public static PacketReceiveProfile Acquire(int packetId) - { - if (!_profiles.TryGetValue(packetId, out var prof)) - { - _profiles.Add(packetId, prof = new PacketReceiveProfile(packetId)); - } - - return prof; - } -} diff --git a/Projects/Server/Diagnostics/TargetProfile.cs b/Projects/Server/Diagnostics/TargetProfile.cs deleted file mode 100644 index 32971c341a..0000000000 --- a/Projects/Server/Diagnostics/TargetProfile.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Server.Diagnostics; - -public class TargetProfile : BaseProfile -{ - private static readonly Dictionary _profiles = new(); - - public TargetProfile(Type type) - : base(type.FullName) - { - } - - public static IEnumerable Profiles => _profiles.Values; - - public static TargetProfile Acquire(Type type) - { - if (!Core.Profiling) - { - return null; - } - - if (!_profiles.TryGetValue(type, out var prof)) - { - _profiles.Add(type, prof = new TargetProfile(type)); - } - - return prof; - } -} diff --git a/Projects/Server/Diagnostics/TimerProfile.cs b/Projects/Server/Diagnostics/TimerProfile.cs deleted file mode 100644 index c1ba30d91a..0000000000 --- a/Projects/Server/Diagnostics/TimerProfile.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; -using System.IO; - -namespace Server.Diagnostics; - -public class TimerProfile : BaseProfile -{ - private static readonly Dictionary _profiles = new(); - - public TimerProfile(string name) - : base(name) - { - } - - public static IEnumerable Profiles => _profiles.Values; - - public long Created { get; set; } - - public long Started { get; set; } - - public long Stopped { get; set; } - - public static TimerProfile Acquire(string name) - { - if (!Core.Profiling) - { - return null; - } - - if (!_profiles.TryGetValue(name, out var prof)) - { - _profiles.Add(name, prof = new TimerProfile(name)); - } - - return prof; - } - - public override void WriteTo(TextWriter op) - { - base.WriteTo(op); - - op.Write("\t{0,12:N0} {1,12:N0} {2,-12:N0}", Created, Started, Stopped); - } -} diff --git a/Projects/Server/Events/CharacterCreatedEvent.cs b/Projects/Server/Events/CharacterCreatedEvent.cs deleted file mode 100644 index 59dd626a6f..0000000000 --- a/Projects/Server/Events/CharacterCreatedEvent.cs +++ /dev/null @@ -1,93 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: CharacterCreatedEvent.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.Runtime.CompilerServices; -using Server.Accounting; -using Server.Network; - -namespace Server; - -public class CharacterCreatedEventArgs -{ - public CharacterCreatedEventArgs( - NetState state, IAccount a, string name, bool female, int hue, StatNameValue[] stats, CityInfo city, - SkillNameValue[] skills, int shirtHue, int pantsHue, int hairID, int hairHue, int beardID, int beardHue, - int profession, Race race - ) - { - State = state; - Account = a; - Name = name; - Female = female; - Hue = hue; - Stats = stats; - City = city; - Skills = skills; - ShirtHue = shirtHue; - PantsHue = pantsHue; - HairID = hairID; - HairHue = hairHue; - BeardID = beardID; - BeardHue = beardHue; - Profession = profession; - Race = race; - } - - public NetState State { get; } - - public IAccount Account { get; } - - public Mobile Mobile { get; set; } - - public string Name { get; } - - public bool Female { get; } - - public int Hue { get; } - - public StatNameValue[] Stats { get; } - - public CityInfo City { get; } - - public SkillNameValue[] Skills { get; } - - public int ShirtHue { get; } - - public int PantsHue { get; } - - public int HairID { get; } - - public int HairHue { get; } - - public int BeardID { get; } - - public int BeardHue { get; } - - public int Profession { get; set; } - - public Race Race { get; } -} - -public readonly record struct SkillNameValue(SkillName Name, int Value); -public readonly record struct StatNameValue(StatType Name, int Value); - -public static partial class EventSink -{ - public static event Action CharacterCreated; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InvokeCharacterCreated(CharacterCreatedEventArgs e) => CharacterCreated?.Invoke(e); -} diff --git a/Projects/Server/Events/EventSink.cs b/Projects/Server/Events/EventSink.cs index 129e256e7a..6a72185f4c 100644 --- a/Projects/Server/Events/EventSink.cs +++ b/Projects/Server/Events/EventSink.cs @@ -34,14 +34,6 @@ public static partial class EventSink public static event Action Disconnected; public static void InvokeDisconnected(Mobile m) => Disconnected?.Invoke(m); - public static event Action PlayerDeath; - public static void InvokePlayerDeath(Mobile m) => PlayerDeath?.Invoke(m); - - public static event Action VirtueMacroRequest; - - public static void InvokeVirtueMacroRequest(Mobile mobile, int virtueID) => - VirtueMacroRequest?.Invoke(mobile, virtueID); - public static event Action PaperdollRequest; public static void InvokePaperdollRequest(Mobile beholder, Mobile beheld) => @@ -49,9 +41,4 @@ public static void InvokePaperdollRequest(Mobile beholder, Mobile beheld) => public static event Action ServerStarted; public static void InvokeServerStarted() => ServerStarted?.Invoke(); - - public static event Action TargetByResourceMacro; - - public static void InvokeTargetByResourceMacro(Mobile m, Item item, short resourceType) => - TargetByResourceMacro?.Invoke(m, item, resourceType); } diff --git a/Projects/Server/Events/ServerListEvent.cs b/Projects/Server/Events/ServerListEvent.cs deleted file mode 100644 index ce1c88ed11..0000000000 --- a/Projects/Server/Events/ServerListEvent.cs +++ /dev/null @@ -1,59 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: ServerListEvent.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; -using System.Runtime.CompilerServices; -using Server.Accounting; -using Server.Network; - -namespace Server; - -public class ServerListEventArgs -{ - public ServerListEventArgs(NetState state, IAccount account) - { - State = state; - Account = account; - Servers = new List(); - } - - public NetState State { get; } - - public IAccount Account { get; } - - public bool Rejected { get; set; } - - public List Servers { get; } - - public void AddServer(string name, IPEndPoint address) - { - AddServer(name, 0, TimeZoneInfo.Local, address); - } - - public void AddServer(string name, int fullPercent, TimeZoneInfo tz, IPEndPoint address) - { - Servers.Add(new ServerInfo(name, fullPercent, tz, address)); - } -} - -public static partial class EventSink -{ - public static event Action ServerList; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InvokeServerList(ServerListEventArgs e) => ServerList?.Invoke(e); -} diff --git a/Projects/Server/Events/GameLoginEvent.cs b/Projects/Server/Events/SocketConnectionEvent.cs similarity index 66% rename from Projects/Server/Events/GameLoginEvent.cs rename to Projects/Server/Events/SocketConnectionEvent.cs index 5d05f180a6..9ac96dcd38 100644 --- a/Projects/Server/Events/GameLoginEvent.cs +++ b/Projects/Server/Events/SocketConnectionEvent.cs @@ -2,7 +2,7 @@ * ModernUO * * Copyright 2019-2023 - ModernUO Development Team * * Email: hi@modernuo.com * - * File: GameLoginEvent.cs * + * File: SocketConnectionEvent.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 * @@ -14,35 +14,28 @@ *************************************************************************/ using System; +using System.Net.Sockets; using System.Runtime.CompilerServices; -using Server.Network; namespace Server; -public class GameLoginEventArgs +public class SocketConnectEventArgs { - public GameLoginEventArgs(NetState state, string un, string pw) + public SocketConnectEventArgs(Socket c) { - State = state; - Username = un; - Password = pw; + Connection = c; + AllowConnection = true; } - public NetState State { get; } + public Socket Connection { get; } - public string Username { get; } - - public string Password { get; } - - public bool Accepted { get; set; } - - public CityInfo[] CityInfo { get; set; } + public bool AllowConnection { get; set; } } public static partial class EventSink { - public static event Action GameLogin; + public static event Action SocketConnect; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InvokeGameLogin(GameLoginEventArgs e) => GameLogin?.Invoke(e); + public static void InvokeSocketConnect(SocketConnectEventArgs e) => SocketConnect?.Invoke(e); } diff --git a/Projects/Server/Guild.cs b/Projects/Server/Guild.cs index ab11330c38..8998860cf1 100644 --- a/Projects/Server/Guild.cs +++ b/Projects/Server/Guild.cs @@ -52,6 +52,10 @@ protected BaseGuild() [CommandProperty(AccessLevel.GameMaster, readOnly: true)] public DateTime Created { get; set; } = Core.Now; + public byte SerializedThread { get; set; } + public int SerializedPosition { get; set; } + public int SerializedLength { get; set; } + public abstract void Serialize(IGenericWriter writer); public abstract void Deserialize(IGenericReader reader); diff --git a/Projects/Server/IEntity.cs b/Projects/Server/IEntity.cs index 4ebc265626..95b11843b2 100644 --- a/Projects/Server/IEntity.cs +++ b/Projects/Server/IEntity.cs @@ -115,6 +115,10 @@ public void Deserialize(IGenericReader reader) Timer.StartTimer(Delete); } + public byte SerializedThread { get; set; } + public int SerializedPosition { get; set; } + public int SerializedLength { get; set; } + public void Serialize(IGenericWriter writer) { } diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index 368991f2a0..aeab60811f 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -802,6 +802,10 @@ public virtual void GetProperties(IPropertyList list) [CommandProperty(AccessLevel.Counselor)] public Serial Serial { get; } + public byte SerializedThread { get; set; } + public int SerializedPosition { get; set; } + public int SerializedLength { get; set; } + public virtual void Serialize(IGenericWriter writer) { writer.Write(9); // version diff --git a/Projects/Server/Items/VirtualHair.cs b/Projects/Server/Items/VirtualHair.cs index 89310e21a5..ce67350738 100644 --- a/Projects/Server/Items/VirtualHair.cs +++ b/Projects/Server/Items/VirtualHair.cs @@ -1,6 +1,5 @@ using System; using System.Buffers; -using System.Runtime.CompilerServices; using ModernUO.Serialization; using Server.Network; diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index cdc30fa3df..13577440d0 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -44,9 +44,6 @@ public static class Core private static bool _crashed; private static string _baseDirectory; - private static bool _profiling; - private static long _profileStart; - private static long _profileTime; private static bool? _isRunningFromXUnit; private static int _itemCount; @@ -90,30 +87,6 @@ public static bool IsRunningFromXUnit } #nullable restore - public static bool Profiling - { - get => _profiling; - set - { - if (_profiling == value) - { - return; - } - - _profiling = value; - - if (_profileStart > 0) - { - _profileTime += Stopwatch.GetTimestamp() - _profileStart; - } - - _profileStart = _profiling ? Stopwatch.GetTimestamp() : 0; - } - } - - public static TimeSpan ProfileTime => - TimeSpan.FromTicks(_profileStart > 0 ? _profileTime + (Stopwatch.GetTimestamp() - _profileStart) : _profileTime); - public static Assembly ApplicationAssembly { get; set; } public static Assembly Assembly { get; set; } @@ -126,50 +99,15 @@ public static bool Profiling private static long _firstTick; - [ThreadStatic] private static long _tickCount; - // Don't access this from other threads than the game thread. private static DateTime _now; - // For Unix Stopwatch.Frequency is normalized to 1ns - // We don't anticipate needing this for Windows/OSX - private const long _maxTickCountBeforePrecisionLoss = long.MaxValue / 1000L; - private static readonly long _ticksPerMillisecond = Stopwatch.Frequency / 1000L; + public static long TickCount => _tickCount; - public static long TickCount - { - get - { - if (_tickCount != 0) - { - return _tickCount; - } + public static DateTime Now => _now; - var timestamp = Stopwatch.GetTimestamp(); - - return timestamp > _maxTickCountBeforePrecisionLoss - ? timestamp / _ticksPerMillisecond - // No precision loss - : 1000L * timestamp / Stopwatch.Frequency; - } - // Setting this to a value lower than the previous is bad. Timers will become delayed - // until time catches up. - set => _tickCount = value; - } - - public static DateTime Now - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - // See notes above for _now and why this is a volatile variable. - var now = _now; - return now == DateTime.MinValue ? DateTime.UtcNow : now; - } - } - - public static long Uptime => Thread.CurrentThread != Thread ? 0 : TickCount - _firstTick; + public static long Uptime => TickCount - _firstTick; private static long _cycleIndex; private static readonly double[] _cyclesPerSecond = new double[128]; @@ -207,21 +145,6 @@ public static string BaseDirectory public static bool Closing => ClosingTokenSource.IsCancellationRequested; - public static string Arguments - { - get - { - var sb = new StringBuilder(); - - if (_profiling) - { - Utility.Separate(sb, "-profile", " "); - } - - return sb.ToString(); - } - } - public static int GlobalUpdateRange { get; set; } = 18; public static int GlobalMaxUpdateRange { get; set; } = 24; @@ -380,7 +303,7 @@ internal static void DoKill(bool restart = false) logger.Information("Restarting"); if (IsWindows) { - using var process = Process.Start("dotnet", $"{ApplicationAssembly.Location} {Arguments}"); + using var process = Process.Start("dotnet", $"{ApplicationAssembly.Location}"); } else { @@ -388,7 +311,7 @@ internal static void DoKill(bool restart = false) process.StartInfo = new ProcessStartInfo { FileName = "dotnet", - Arguments = $"{ApplicationAssembly.Location} {Arguments}", + Arguments = $"{ApplicationAssembly.Location}", UseShellExecute = true }; @@ -413,6 +336,8 @@ private static void HandleClosed() World.WaitForWriteCompletion(); World.ExitSerializationThreads(); + PingServer.Shutdown(); + TcpServer.Shutdown(); if (!_crashed) { @@ -420,7 +345,10 @@ private static void HandleClosed() } } - public static void Setup(bool profiling, Assembly applicationAssembly, Process process) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long GetTimestamp() => 1000L * Stopwatch.GetTimestamp() / Stopwatch.Frequency; + + public static void Setup(Assembly applicationAssembly, Process process) { Process = process; ApplicationAssembly = applicationAssembly; @@ -428,7 +356,6 @@ public static void Setup(bool profiling, Assembly applicationAssembly, Process p Thread = Thread.CurrentThread; LoopContext = new EventLoopContext(); SynchronizationContext.SetSynchronizationContext(LoopContext); - Profiling = profiling; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit; @@ -472,13 +399,6 @@ You should have received a copy of the GNU General Public License logger.Information("Running on {Framework}", RuntimeInformation.FrameworkDescription); - var s = Arguments; - - if (s.Length > 0) - { - logger.Information("Running with arguments: {Args}", s); - } - var assemblyPath = Path.Join(BaseDirectory, AssembliesConfiguration); // Load UOContent.dll @@ -497,7 +417,10 @@ You should have received a copy of the GNU General Public License VerifySerialization(); - Timer.Init(TickCount); + _now = DateTime.UtcNow; + _firstTick = _tickCount = GetTimestamp(); + + Timer.Init(_tickCount); AssemblyHandler.Invoke("Configure"); @@ -511,7 +434,6 @@ You should have received a copy of the GNU General Public License TcpServer.Start(); PingServer.Start(); EventSink.InvokeServerStarted(); - _firstTick = TickCount; RunEventLoop(); } @@ -526,7 +448,7 @@ public static void RunEventLoop() #endif var cycleCount = _cyclesPerSecond.Length; - long last = Stopwatch.GetTimestamp(); + long last = _tickCount; const int interval = 100; double frequency = Stopwatch.Frequency * interval; @@ -534,7 +456,7 @@ public static void RunEventLoop() while (!Closing) { - _tickCount = TickCount; + _tickCount = GetTimestamp(); _now = DateTime.UtcNow; Mobile.ProcessDeltaQueue(); @@ -543,7 +465,6 @@ public static void RunEventLoop() // Handle networking NetState.Slice(); - // PingServer.Slice(); // Execute captured post-await methods (like Timer.Pause) LoopContext.ExecuteTasks(); @@ -563,12 +484,10 @@ public static void RunEventLoop() break; } - _tickCount = 0; - _now = DateTime.MinValue; - - if (++sample % interval == 0) + if (sample++ == interval) { - var now = Stopwatch.GetTimestamp(); + sample = 0; + var now = GetTimestamp(); var cyclesPerSecond = frequency / (now - last); _cyclesPerSecond[_cycleIndex++] = cyclesPerSecond; diff --git a/Projects/Server/Maps/Map.ClientEnumerator.cs b/Projects/Server/Maps/Map.ClientEnumerator.cs index 4321a0fc5d..ae53010a1b 100644 --- a/Projects/Server/Maps/Map.ClientEnumerator.cs +++ b/Projects/Server/Maps/Map.ClientEnumerator.cs @@ -195,11 +195,14 @@ public MobileEnumerator(Map map, Rectangle2D bounds, bool makeBoundsInclusive) _bounds = bounds; - map.CalculateSectors(bounds, out _sectorStartX, out var _sectorStartY, out _sectorEndX, out _sectorEndY); + if (map != null) + { + map.CalculateSectors(bounds, out _sectorStartX, out var _sectorStartY, out _sectorEndX, out _sectorEndY); - // We start the X sector one short because it gets incremented immediately in MoveNext() - _currentSectorX = _sectorStartX - 1; - _currentSectorY = _sectorStartY; + // We start the X sector one short because it gets incremented immediately in MoveNext() + _currentSectorX = _sectorStartX - 1; + _currentSectorY = _sectorStartY; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/Server/Maps/Map.ItemEnumerator.cs b/Projects/Server/Maps/Map.ItemEnumerator.cs index 9eab2effb9..4bf8f42029 100644 --- a/Projects/Server/Maps/Map.ItemEnumerator.cs +++ b/Projects/Server/Maps/Map.ItemEnumerator.cs @@ -226,11 +226,14 @@ public ItemEnumerator(Map map, Rectangle2D bounds, bool makeBoundsInclusive) _bounds = bounds; - map.CalculateSectors(bounds, out _sectorStartX, out var _sectorStartY, out _sectorEndX, out _sectorEndY); + if (map != null) + { + map.CalculateSectors(bounds, out _sectorStartX, out var _sectorStartY, out _sectorEndX, out _sectorEndY); - // We start the X sector one short because it gets incremented immediately in MoveNext() - _currentSectorX = _sectorStartX - 1; - _currentSectorY = _sectorStartY; + // We start the X sector one short because it gets incremented immediately in MoveNext() + _currentSectorX = _sectorStartX - 1; + _currentSectorY = _sectorStartY; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/Server/Maps/Map.MobileEnumerator.cs b/Projects/Server/Maps/Map.MobileEnumerator.cs index 9e7edee15e..cac64ce3a6 100644 --- a/Projects/Server/Maps/Map.MobileEnumerator.cs +++ b/Projects/Server/Maps/Map.MobileEnumerator.cs @@ -225,11 +225,14 @@ public MobileEnumerator(Map map, Rectangle2D bounds, bool makeBoundsInclusive) _bounds = bounds; - map.CalculateSectors(bounds, out _sectorStartX, out var _sectorStartY, out _sectorEndX, out _sectorEndY); + if (map != null) + { + map.CalculateSectors(bounds, out _sectorStartX, out var _sectorStartY, out _sectorEndX, out _sectorEndY); - // We start the X sector one short because it gets incremented immediately in MoveNext() - _currentSectorX = _sectorStartX - 1; - _currentSectorY = _sectorStartY; + // We start the X sector one short because it gets incremented immediately in MoveNext() + _currentSectorX = _sectorStartX - 1; + _currentSectorY = _sectorStartY; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/Server/Maps/Map.MultiEnumerator.cs b/Projects/Server/Maps/Map.MultiEnumerator.cs index d7e9f9eaf4..bc1f7b018b 100644 --- a/Projects/Server/Maps/Map.MultiEnumerator.cs +++ b/Projects/Server/Maps/Map.MultiEnumerator.cs @@ -189,12 +189,15 @@ public MultiBoundsEnumerator(Map map, Rectangle2D bounds, bool makeBoundsInclusi _bounds = bounds; - map.CalculateSectors(bounds, out _sectorStartX, out var _sectorStartY, out _sectorEndX, out _sectorEndY); + if (map != null) + { + map.CalculateSectors(bounds, out _sectorStartX, out var _sectorStartY, out _sectorEndX, out _sectorEndY); - // We start the X sector one short because it gets incremented immediately in MoveNext() - _currentSectorX = _sectorStartX - 1; - _currentSectorY = _sectorStartY; - _index = 0; + // We start the X sector one short because it gets incremented immediately in MoveNext() + _currentSectorX = _sectorStartX - 1; + _currentSectorY = _sectorStartY; + _index = 0; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index adcdfbc606..d6b9eb9a24 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -59,7 +59,7 @@ public class DamageEntry } [Flags] -public enum StatType +public enum StatType : byte { Str = 1, Dex = 2, @@ -2277,6 +2277,10 @@ public virtual void GetProperties(IPropertyList list) [CommandProperty(AccessLevel.Counselor)] public Serial Serial { get; } + public byte SerializedThread { get; set; } + public int SerializedPosition { get; set; } + public int SerializedLength { get; set; } + public virtual void Serialize(IGenericWriter writer) { writer.Write(36); // version @@ -2847,6 +2851,7 @@ public virtual void ProcessDelta() Layer.FacialHair ); } + ourState.Send(facialHairPacket); } @@ -2984,6 +2989,7 @@ public virtual void ProcessDelta() Layer.FacialHair ); } + state.Send(facialHairPacket); } @@ -3429,7 +3435,6 @@ public virtual void ValidateSkillMods() Skills[mod.Skill]?.Update(); } } - } public virtual void AddSkillMod(SkillMod mod) @@ -4826,37 +4831,34 @@ public virtual void OnDeath(Container c) if (!m_Player) { Delete(); + return; } - else - { - m_NetState.SendDeathStatus(); - Warmode = false; + m_NetState.SendDeathStatus(); - BodyMod = 0; - // Body = this.Female ? 0x193 : 0x192; - Body = Race.GhostBody(this); + Warmode = false; - var deathShroud = new Item(0x204E) { Movable = false, Layer = Layer.OuterTorso }; + BodyMod = 0; + // Body = this.Female ? 0x193 : 0x192; + Body = Race.GhostBody(this); - AddItem(deathShroud); + var deathShroud = new Item(0x204E) { Movable = false, Layer = Layer.OuterTorso }; - Items.Remove(deathShroud); - Items.Insert(0, deathShroud); + AddItem(deathShroud); - Poison = null; - Combatant = null; + Items.Remove(deathShroud); + Items.Insert(0, deathShroud); - Hits = 0; - Stam = 0; - Mana = 0; + Poison = null; + Combatant = null; - EventSink.InvokePlayerDeath(this); + Hits = 0; + Stam = 0; + Mana = 0; - ProcessDelta(); + ProcessDelta(); - CheckStatTimers(); - } + CheckStatTimers(); } public virtual bool CheckTarget(Mobile from, Target targ, object targeted) => true; @@ -5070,7 +5072,8 @@ public virtual void Lift(Item item, int amount, out bool rejected, out LRReason { var rootItem = root as Item; - Span buffer = stackalloc byte[OutgoingPlayerPackets.DragEffectPacketLength].InitializePacket(); + Span buffer = stackalloc byte[OutgoingPlayerPackets.DragEffectPacketLength] + .InitializePacket(); foreach (var ns in map.GetClientsInRange(from.Location)) { @@ -5588,7 +5591,8 @@ public virtual void DoSpeech(string text, int[] keywords, MessageType type, int ProcessDelta(); Span regBuffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(text)].InitializePacket(); - Span mutBuffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(mutatedText)].InitializePacket(); + Span mutBuffer = + stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(mutatedText)].InitializePacket(); // TODO: Should this be sorted like onSpeech is below? for (var i = 0; i < hears.Count; ++i) @@ -5598,7 +5602,16 @@ public virtual void DoSpeech(string text, int[] keywords, MessageType type, int if (mutatedArgs == null || !CheckHearsMutatedSpeech(heard, mutateContext)) { var length = OutgoingMessagePackets.CreateMessage( - regBuffer, Serial, Body, type, hue, 3, false, m_Language, Name, text + regBuffer, + Serial, + Body, + type, + hue, + 3, + false, + m_Language, + Name, + text ); if (length != regBuffer.Length) @@ -5612,7 +5625,16 @@ public virtual void DoSpeech(string text, int[] keywords, MessageType type, int else { var length = OutgoingMessagePackets.CreateMessage( - mutBuffer, Serial, Body, type, hue, 3, false, m_Language, Name, mutatedText + mutBuffer, + Serial, + Body, + type, + hue, + 3, + false, + m_Language, + Name, + mutatedText ); if (length != mutBuffer.Length) @@ -6899,10 +6921,12 @@ public void SendEverything() } } - var range = new Rectangle2D(m_Location.X - Core.GlobalMaxUpdateRange, + var range = new Rectangle2D( + m_Location.X - Core.GlobalMaxUpdateRange, m_Location.Y - Core.GlobalMaxUpdateRange, Core.GlobalMaxUpdateRange * 2 + 1, - Core.GlobalMaxUpdateRange * 2 + 1); + Core.GlobalMaxUpdateRange * 2 + 1 + ); foreach (var multi in m_Map.GetMultisInBounds(range)) { @@ -8072,7 +8096,8 @@ public Map.MobileBoundsEnumerable GetMobilesInRange(int range) where T : M m_Map == null ? Map.MobileBoundsEnumerable.Empty : m_Map.GetMobilesInRange(m_Location, range); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Map.ClientAtEnumerable GetClientsAt() => m_Map == null ? Map.ClientAtEnumerable.Empty : Map.GetClientsAt(m_Location); + public Map.ClientAtEnumerable GetClientsAt() => + m_Map == null ? Map.ClientAtEnumerable.Empty : Map.GetClientsAt(m_Location); [MethodImpl(MethodImplOptions.AggressiveInlining)] public Map.ClientBoundsEnumerable GetClientsInRange(int range) => @@ -8852,7 +8877,16 @@ public void PublicOverheadMessage( ) { var length = OutgoingMessagePackets.CreateMessage( - buffer, Serial, Body, type, hue, 3, ascii, Language, Name, text + buffer, + Serial, + Body, + type, + hue, + 3, + ascii, + Language, + Name, + text ); if (length != buffer.Length) @@ -8879,7 +8913,15 @@ public void PublicOverheadMessage(MessageType type, int hue, int number, string if (state.Mobile.CanSee(this) && (noLineOfSight || state.Mobile.InLOS(this))) { var length = OutgoingMessagePackets.CreateMessageLocalized( - buffer, Serial, Body, type, hue, 3, number, Name, args + buffer, + Serial, + Body, + type, + hue, + 3, + number, + Name, + args ); if (length != buffer.Length) @@ -8915,7 +8957,17 @@ public void PublicOverheadMessage( ) { var length = OutgoingMessagePackets.CreateMessageLocalizedAffix( - buffer, Serial, Body, type, hue, 3, number, Name, affixType, affix, args + buffer, + Serial, + Body, + type, + hue, + 3, + number, + Name, + affixType, + affix, + args ); if (length != buffer.Length) @@ -8959,7 +9011,15 @@ public void NonlocalOverheadMessage(MessageType type, int hue, int number, strin if (state != m_NetState && state.Mobile.CanSee(this)) { var length = OutgoingMessagePackets.CreateMessageLocalized( - buffer, Serial, Body, type, hue, 3, number, Name, args + buffer, + Serial, + Body, + type, + hue, + 3, + number, + Name, + args ); if (length != buffer.Length) @@ -8986,7 +9046,16 @@ public void NonlocalOverheadMessage(MessageType type, int hue, bool ascii, strin if (state != m_NetState && state.Mobile.CanSee(this)) { var length = OutgoingMessagePackets.CreateMessage( - buffer, Serial, Body, type, hue, 3, ascii, Language, Name, text + buffer, + Serial, + Body, + type, + hue, + 3, + ascii, + Language, + Name, + text ); if (length != buffer.Length) diff --git a/Projects/Server/Network/Firewall/Firewall.cs b/Projects/Server/Network/Firewall/Firewall.cs index f35046d40d..e800504165 100644 --- a/Projects/Server/Network/Firewall/Firewall.cs +++ b/Projects/Server/Network/Firewall/Firewall.cs @@ -14,83 +14,21 @@ *************************************************************************/ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Server.Logging; namespace Server.Network; public static class Firewall { - private static readonly ILogger logger = LogFactory.GetLogger(typeof(Firewall)); - private static InternalValidationEntry _validationEntry; private static readonly Dictionary _isBlockedCache = new(); - private static readonly ConcurrentQueue<(IFirewallEntry FirewallyEntry, bool Remove)> _firewallQueue = new(); private static readonly SortedSet _firewallSet = new(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IFirewallEntry RequestAddSingleIPEntry(string entry) - { - try - { - var firewallEntry = new SingleIpFirewallEntry(entry); - _firewallQueue.Enqueue((firewallEntry, false)); - return firewallEntry; - } - catch (Exception e) - { - logger.Warning(e, "Failed to add firewall entry: {Pattern}", entry); - return null; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IFirewallEntry RequestAddCIDREntry(string entry) - { - try - { - var firewallEntry = new CidrFirewallEntry(entry); - _firewallQueue.Enqueue((firewallEntry, false)); - return firewallEntry; - } - catch (Exception e) - { - logger.Warning(e, "Failed to add firewall entry: {Pattern}", entry); - return null; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RequestAddEntry(IFirewallEntry entry) - { - _firewallQueue.Enqueue((entry, false)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RequestRemoveEntry(IFirewallEntry entry) - { - _firewallQueue.Enqueue((entry, true)); - } - - internal static void ProcessQueue() - { - while (_firewallQueue.TryDequeue(out var entry)) - { - if (entry.Remove) - { - RemoveEntry(entry.FirewallyEntry); - } - else - { - AddEntry(entry.FirewallyEntry); - } - } - } + public static SortedSet FirewallSet => _firewallSet; internal static bool IsBlocked(IPAddress address) { @@ -128,22 +66,32 @@ internal static bool IsBlocked(IPAddress address) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddEntry(IFirewallEntry firewallEntry) + public static bool Add(IFirewallEntry firewallEntry) { - _firewallSet.Add(firewallEntry); - _isBlockedCache.Clear(); + if (_firewallSet.Add(firewallEntry)) + { + _isBlockedCache.Clear(); + return true; + } + + return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void RemoveEntry(IFirewallEntry entry) + public static bool Remove(IFirewallEntry entry) { if (entry == null) { - return; + return false; + } + + if (_firewallSet.Remove(entry)) + { + _isBlockedCache.Clear(); + return true; } - _firewallSet.Remove(entry); - _isBlockedCache.Clear(); + return false; } private class InternalValidationEntry : BaseFirewallEntry diff --git a/Projects/Server/Network/IPLimiter.cs b/Projects/Server/Network/IPLimiter.cs index 046ccd639e..dc4717645d 100644 --- a/Projects/Server/Network/IPLimiter.cs +++ b/Projects/Server/Network/IPLimiter.cs @@ -51,22 +51,32 @@ public static bool Verify(IPAddress ourAddress) var now = Core.Now; - CheckThrottledAddresses(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 var accessLog)) + if (_connectionAttempts.TryGetValue(_accessCheck, out accessLog)) { _connectionAttempts.Remove(accessLog); accessLog.Count++; + accessLog.Expiration = now + ConnectionAttemptsDuration; if (now <= accessLog.Expiration && accessLog.Count >= MaxConnections) { - BlockConnection(now, accessLog); + _throttledAddresses.Add(accessLog); return false; } - - accessLog.Expiration = now + ConnectionAttemptsDuration; } else { @@ -79,26 +89,6 @@ public static bool Verify(IPAddress ourAddress) return true; } - private static void BlockConnection(DateTime now, IPAccessLog accessLog) - { - accessLog.Expiration = now + ConnectionAttemptsDuration; - _throttledAddresses.Add(accessLog); - } - - private static void CheckThrottledAddresses(DateTime now) - { - while (_throttledAddresses.Count > 0) - { - var accessLog = _throttledAddresses.Min; - if (now <= accessLog.Expiration) - { - break; - } - - _throttledAddresses.Remove(accessLog); - } - } - private class IPAccessLog : IComparable { public IPAddress IPAddress; diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index 51ef3eb0ef..172be42aaf 100755 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -15,7 +15,6 @@ using Server.Accounting; using Server.Collections; -using Server.Diagnostics; using Server.HuePickers; using Server.Items; using Server.Logging; @@ -33,8 +32,6 @@ namespace Server.Network; -public delegate void NetStateCreatedCallback(NetState ns); - public delegate void DecodePacket(Span buffer, ref int length); public delegate int EncodePacket(ReadOnlySpan inputBuffer, Span outputBuffer); @@ -57,8 +54,6 @@ public partial class NetState : IComparable, IValueLinkListNode _throttled = new(256); private static readonly Queue _throttledPending = new(256); - public static NetStateCreatedCallback CreatedCallback { get; set; } - private static readonly SortedSet _connecting = new(NetStateConnectingComparer.Instance); private static readonly HashSet _instances = new(2048); public static IReadOnlySet Instances => _instances; @@ -69,8 +64,8 @@ public partial class NetState : IComparable, IValueLinkListNode= 0 and < 0x100) { + _packetThrottles ??= new long[0x100]; _packetThrottles[packetID] = Core.TickCount; } } - public long GetPacketTime(int packetID) => packetID is >= 0 and < 0x100 ? _packetThrottles[packetID] : 0; + public long GetPacketTime(int packetID) => + packetID is >= 0 and < 0x100 && _packetThrottles != null ? _packetThrottles[packetID] : 0; private void UpdatePacketCount(int packetID) { if (packetID is >= 0 and < 0x100) { + _packetCounts ??= new long[0x100]; _packetCounts[packetID]++; } } public int CheckPacketCounts() { + if (_packetCounts == null) + { + return 0; + } + for (int i = 0; i < _packetCounts.Length; i++) { long count = _packetCounts[i]; @@ -455,14 +462,6 @@ public void Send(ReadOnlySpan span) try { - PacketSendProfile prof = null; - - if (Core.Profiling) - { - prof = PacketSendProfile.Acquire(span[0]); - prof.Start(); - } - if (_packetEncoder != null) { length = _packetEncoder(span, buffer); @@ -484,8 +483,6 @@ public void Send(ReadOnlySpan span) _flushPending.Enqueue(this); _flushQueued = true; } - - prof?.Finish(); } catch (Exception ex) { @@ -594,7 +591,7 @@ public void HandleReceive(bool throttled = false) if (newSeed == 0) { - HandleError(0, 0); + Disconnect(string.Empty); return; } @@ -605,16 +602,16 @@ public void HandleReceive(bool throttled = false) _parserState = ParserState.AwaitingNextPacket; _protocolState = ProtocolState.GameServer_AwaitingGameServerLogin; } - else + else // Don't allow partial packets on initial connection, just disconnect them. { - _parserState = ParserState.AwaitingPartialPacket; + Disconnect(string.Empty); } break; } case ProtocolState.LoginServer_AwaitingLogin: { - if (packetId != 0xCF && packetId != 0x80) + if (packetId != 0x80) { LogInfo("Possible encrypted client detected, disconnecting..."); HandleError(packetId, packetLength); @@ -662,7 +659,12 @@ public void HandleReceive(bool throttled = false) case ProtocolState.GameServer_AwaitingGameServerLogin: { - if (packetId != 0x91 && packetId != 0x80) + if (packetId == 0x80) + { + goto case ProtocolState.LoginServer_AwaitingLogin; + } + + if (packetId != 0x91) { HandleError(packetId, packetLength); return; @@ -804,14 +806,6 @@ private unsafe ParserState HandlePacket(SpanReader packetReader, byte packetId, SetPacketTime(packetId); } - PacketReceiveProfile prof = null; - - if (Core.Profiling) - { - prof = PacketReceiveProfile.Acquire(packetId); - prof?.Start(); - } - UpdatePacketCount(packetId); if (PacketLogging) @@ -826,8 +820,6 @@ private unsafe ParserState HandlePacket(SpanReader packetReader, byte packetId, handler.OnReceive(this, new SpanReader(packetReader.Buffer.Slice(start, remainingLength))); - prof?.Finish(packetLength); - return ParserState.AwaitingNextPacket; } @@ -938,7 +930,7 @@ private static void DisconnectUnattachedSockets() var ns = _connecting.Min; var socketTime = ns.ConnectedOn; - // If the socket has been connected for less than 2 seconds, we can stop checking + // If the socket has been connected for less than the limit, we can stop checking if (now - socketTime < ConnectingSocketIdleLimit) { break; @@ -967,17 +959,6 @@ public static void Slice() { DisconnectUnattachedSockets(); - const int maxEntriesPerLoop = 32; - var count = 0; - while (++count <= maxEntriesPerLoop && TcpServer.ConnectedQueue.TryDequeue(out var ns)) - { - CreatedCallback?.Invoke(ns); - - _instances.Add(ns); - _connecting.Add(ns); // Add to the connecting set, and remove them when they authenticated. - ns.LogInfo($"Connected. [{Instances.Count} Online]"); - } - while (_throttled.Count > 0) { var ns = _throttled.Dequeue(); @@ -993,7 +974,7 @@ public static void Slice() _throttled.Enqueue(_throttledPending.Dequeue()); } - count = _pollGroup.Poll(_polledStates); + var count = _pollGroup.Poll(_polledStates); if (count > 0) { diff --git a/Projects/Server/Network/PingServer.cs b/Projects/Server/Network/PingServer.cs index 31db1ad08e..41a59468b7 100644 --- a/Projects/Server/Network/PingServer.cs +++ b/Projects/Server/Network/PingServer.cs @@ -16,7 +16,6 @@ using System.Collections.Generic; using System.Net; using System.Net.Sockets; -using System.Threading; using Server.Logging; namespace Server.Network; @@ -47,8 +46,8 @@ public static void Start() return; } - HashSet listeningAddresses = new HashSet(); - List listeners = new List(); + HashSet listeningAddresses = []; + List listeners = []; foreach (var serverIpep in ServerConfiguration.Listeners) { @@ -70,7 +69,7 @@ public static void Start() } listeners.Add(listener); - new Thread(BeginAcceptingUdpRequest).Start(listener); + BeginAcceptingUdpRequest(listener); } foreach (var ipep in listeningAddresses) @@ -140,4 +139,12 @@ private static async void BeginAcceptingUdpRequest(object state) } } } + + public static void Shutdown() + { + foreach (var listener in Listeners) + { + listener.Close(); + } + } } diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index 8ec399bd0f..be526f7038 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -13,8 +13,6 @@ * along with this program. If not, see . * *************************************************************************/ -using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -22,7 +20,6 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.CompilerServices; -using System.Threading; using Server.Logging; using Server.Misc; @@ -32,30 +29,52 @@ public static class TcpServer { private static readonly ILogger logger = LogFactory.GetLogger(typeof(TcpServer)); - private const long ListenerErrorMessageDelay = 10000; // 10 seconds - - private static long _nextMaximumSocketsReachedMessage; - private static readonly SemaphoreSlim _queueSemaphore = new(0); - private static readonly ConcurrentQueue _connectingQueue = []; - private static Thread _processConnectionsThread; - - // Sanity. 256 * 1024 * 4096 = ~1.3GB of ram - public static int MaxConnections { get; set; } + // AccountLoginReject BadComm + private static readonly byte[] _socketRejected = [0x82, 0xFF]; public static IPEndPoint[] ListeningAddresses { get; private set; } public static Socket[] Listeners { get; private set; } - public static ConcurrentQueue ConnectedQueue { get; } = []; - - public static void Configure() + public static void Start() { - MaxConnections = ServerConfiguration.GetOrUpdateSetting("tcpServer.maxConnections", 4096); + HashSet listeningAddresses = new HashSet(); + List listeners = new List(); + foreach (var ipep in ServerConfiguration.Listeners) + { + var listener = CreateListener(ipep); + if (listener == null) + { + continue; + } + + if (ipep.Address.Equals(IPAddress.Any) || ipep.Address.Equals(IPAddress.IPv6Any)) + { + listeningAddresses.UnionWith(GetListeningAddresses(ipep)); + } + else + { + listeningAddresses.Add(ipep); + } + + listeners.Add(listener); + BeginAcceptingSockets(listener); + } + + foreach (var ipep in listeningAddresses) + { + logger.Information("Listening: {Address}:{Port}", ipep.Address, ipep.Port); + } + + ListeningAddresses = listeningAddresses.ToArray(); + Listeners = listeners.ToArray(); } - public static void Start() + public static void Shutdown() { - _processConnectionsThread = new Thread(ProcessConnections); - _processConnectionsThread.Start(); + foreach (var listener in Listeners) + { + listener.Close(); + } } public static IEnumerable GetListeningAddresses(IPEndPoint ipep) => @@ -104,104 +123,48 @@ public static Socket CreateListener(IPEndPoint ipep) return null; } - private static async void BeginAcceptingSockets(object state) + private static async void BeginAcceptingSockets(Socket listener) { - if (state is not Socket listener) - { - return; - } - - var cancellationToken = Core.ClosingTokenSource.Token; - - while (!cancellationToken.IsCancellationRequested) + while (!Core.Closing) { + Socket socket = null; try { - var socket = await listener.AcceptAsync(cancellationToken); - _connectingQueue.Enqueue(socket); - _queueSemaphore.Release(); - } - catch (OperationCanceledException) - { - return; - } - catch - { - // ignored - } - } - - listener.Close(); - } - - private static void ProcessConnections() - { - var cancellationToken = Core.ClosingTokenSource.Token; - HashSet listeningAddresses = []; - List listeners = []; + socket = await listener.AcceptAsync(); + var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address; - foreach (var ipep in ServerConfiguration.Listeners) - { - var listener = CreateListener(ipep); - if (listener == null) - { - continue; - } - - bool added; - - if (ipep.Address.Equals(IPAddress.Any) || ipep.Address.Equals(IPAddress.IPv6Any)) - { - var beforeCount = listeningAddresses.Count; - listeningAddresses.UnionWith(GetListeningAddresses(ipep)); - added = listeningAddresses.Count > beforeCount; - } - else - { - added = listeningAddresses.Add(ipep); - } - - if (added) - { - listeners.Add(listener); - - new Thread(BeginAcceptingSockets).Start(listener); - } - } - - foreach (var ipep in listeningAddresses) - { - logger.Information("Listening: {Address}:{Port}", ipep.Address, ipep.Port); - } - - ListeningAddresses = listeningAddresses.ToArray(); - Listeners = listeners.ToArray(); - - while (true) - { - try - { - while (!cancellationToken.IsCancellationRequested) + if (!IPLimiter.Verify(remoteIP)) { - _queueSemaphore.Wait(cancellationToken); - - Firewall.ProcessQueue(); + TraceDisconnect("Past IP limit threshold", remoteIP); + logger.Debug("{Address} Past IP limit threshold", remoteIP); + } + else if (Firewall.IsBlocked(remoteIP)) + { + TraceDisconnect("Firewalled", remoteIP); + logger.Debug("{Address} Firewalled", remoteIP); + } + else + { + var args = new SocketConnectEventArgs(socket); + EventSink.InvokeSocketConnect(args); - if (_connectingQueue.TryDequeue(out var socket)) + if (args.AllowConnection) { - ProcessConnection(socket); + _ = new NetState(socket); + continue; } + + TraceDisconnect("Rejected by socket event handler", remoteIP); + + // Reject the connection + socket.Send(_socketRejected, SocketFlags.None); } - return; - } - catch (OperationCanceledException) - { - return; + CloseSocket(socket); } - catch (Exception e) + catch { - logger.Error(e, "Error occurred in ProcessConnections"); + CloseSocket(socket); } } } @@ -213,71 +176,9 @@ private static void CloseSocket(Socket socket) { socket.Shutdown(SocketShutdown.Both); } - catch + finally { - // ignored - } - - socket.Close(); - } - - private static void ProcessConnection(Socket socket) - { - try - { - var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address; - - if (NetState.Instances.Count >= MaxConnections) - { - var ticks = Core.TickCount; - - if (ticks - _nextMaximumSocketsReachedMessage > 0) - { - if (socket.RemoteEndPoint is IPEndPoint ipep) - { - var ip = ipep.Address.ToString(); - logger.Warning("{Address} Failed (Maximum connections reached)", ip); - } - - _nextMaximumSocketsReachedMessage = ticks + ListenerErrorMessageDelay; - } - - CloseSocket(socket); - return; - } - - var firewalled = Firewall.IsBlocked(remoteIP); - if (!firewalled) - { - var socketConnectedArgs = new SocketConnectedEventArgs(socket); - EventSink.InvokeSocketConnected(socketConnectedArgs); - firewalled = !socketConnectedArgs.ConnectionAllowed; - } - - if (firewalled) - { - TraceDisconnect("Firewalled", remoteIP); - logger.Debug("{Address} Firewalled", remoteIP); - - CloseSocket(socket); - return; - } - - if (!IPLimiter.Verify(remoteIP)) - { - TraceDisconnect("Past IP limit threshold", remoteIP); - logger.Debug("{Address} Past IP limit threshold", remoteIP); - - CloseSocket(socket); - return; - } - - var ns = new NetState(socket); - ConnectedQueue.Enqueue(ns); - } - catch - { - // ignored + socket.Close(); } } @@ -299,22 +200,4 @@ private static void TraceDisconnect(string reason, IPAddress ip) // ignored } } - - public static class EventSink - { - // IMPORTANT: This is executed asynchronously! Do not run any game thread code on these delegates! - public static event Action SocketConnected; - - internal static void InvokeSocketConnected(SocketConnectedEventArgs context) => - SocketConnected?.Invoke(context); - } - - public class SocketConnectedEventArgs - { - public Socket Socket { get; } - - public bool ConnectionAllowed { get; set; } = true; - - internal SocketConnectedEventArgs(Socket socket) => Socket = socket; - } } diff --git a/Projects/Server/PropertyList/ObjectPropertyList.cs b/Projects/Server/PropertyList/ObjectPropertyList.cs index ae3cf5ae92..5a8a94e975 100644 --- a/Projects/Server/PropertyList/ObjectPropertyList.cs +++ b/Projects/Server/PropertyList/ObjectPropertyList.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: ObjectPropertyList.cs * * * @@ -79,8 +79,9 @@ public void Reset() _stringNumbersIndex = 0; Header = 0; HeaderArgs = null; - STArrayPool.Shared.Return(_arrayToReturnToPool); _pos = 0; + + Dispose(); } private void Flush() @@ -499,13 +500,19 @@ private void GrowCore(uint requiredMinCapacity) public void Dispose() { - STArrayPool.Shared.Return(_arrayToReturnToPool); - _arrayToReturnToPool = null; + if (_arrayToReturnToPool != null) + { + STArrayPool.Shared.Return(_arrayToReturnToPool); + _arrayToReturnToPool = null; + } } ~ObjectPropertyList() { - STArrayPool.Shared.Return(_arrayToReturnToPool); - _arrayToReturnToPool = null; + if (_arrayToReturnToPool != null) + { + STArrayPool.Shared.Return(_arrayToReturnToPool); + _arrayToReturnToPool = null; + } } } diff --git a/Projects/Server/Serialization/AdhocPersistence.cs b/Projects/Server/Serialization/AdhocPersistence.cs index 332f3a83c1..b60e2482ab 100644 --- a/Projects/Server/Serialization/AdhocPersistence.cs +++ b/Projects/Server/Serialization/AdhocPersistence.cs @@ -55,8 +55,8 @@ public static void SerializeAndSnapshot( { var fullPath = PathUtility.GetFullPath(filePath, Core.BaseDirectory); PathUtility.EnsureDirectory(Path.GetDirectoryName(fullPath)); - ConcurrentQueue types = []; - var writer = new MemoryMapFileWriter(new FileStream(filePath, FileMode.Create), sizeHint, types); + HashSet typesSet = []; + var writer = new MemoryMapFileWriter(new FileStream(filePath, FileMode.Create), sizeHint, typesSet); serializer(writer); Task.Run( @@ -67,14 +67,6 @@ public static void SerializeAndSnapshot( writer.Dispose(); fs.Dispose(); - HashSet typesSet = []; - - // Dedupe the queue. - foreach (var type in types) - { - typesSet.Add(type); - } - Persistence.WriteSerializedTypesSnapshot(Path.GetDirectoryName(fullPath), typesSet); }, Core.ClosingTokenSource.Token diff --git a/Projects/Server/Serialization/BinaryFileReader.cs b/Projects/Server/Serialization/BinaryFileReader.cs new file mode 100644 index 0000000000..f540f8e2e9 --- /dev/null +++ b/Projects/Server/Serialization/BinaryFileReader.cs @@ -0,0 +1,198 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2024 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: BinaryFileReader.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.IO; +using System.IO.MemoryMappedFiles; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Server; + +/// +/// Read bits of data from a serialized file in a managed environment. +/// +/// Uses the following components collectively: +///

+///

+///

+///
+///
+public sealed unsafe class BinaryFileReader : IDisposable, IGenericReader +{ + private readonly bool _usePrefixes; + private readonly MemoryMappedFile _mmf; + private readonly MemoryMappedViewStream _accessor; + private readonly UnmanagedDataReader _reader; + + /// + /// Read bits of data from a serialized file in a managed environment. + ///
Encoding is UTF8 if left null.
+ /// + /// Uses the following components collectively: + ///

+ ///

+ ///

+ ///
+ ///
+ /// Full file path of the file to be deserialized. + /// Sets if strings should be read with itern. + /// Set an encoding. By default UTF8 + public BinaryFileReader(string path, bool usePrefixes = true, Encoding encoding = null) + { + _usePrefixes = usePrefixes; + var fi = new FileInfo(path); + + if (fi.Length > 0) + { + _mmf = MemoryMappedFile.CreateFromFile(path, FileMode.Open); + _accessor = _mmf.CreateViewStream(); + byte* ptr = null; + _accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); + _reader = new UnmanagedDataReader(ptr, _accessor.Length, encoding: encoding); + } + else + { + _reader = new UnmanagedDataReader(null, 0, encoding: encoding); + } + } + + /// + /// How many bits deep into the file is the reader at currently. + /// + public long Position => _reader.Position; + + public void Dispose() + { + _accessor?.SafeMemoryMappedViewHandle.ReleasePointer(); + _accessor?.Dispose(); + _mmf?.Dispose(); + } + + /// + /// If usePrefixes is true, return a ReadString(intern) else ReadStringRaw(intern). + /// + /// A string value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadString(bool intern = false) => _usePrefixes ? _reader.ReadString(intern) : _reader.ReadStringRaw(intern); + /// + /// Returns the next set of bits that make up a string. + /// + /// Next string value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadStringRaw(bool intern = false) => _reader.ReadStringRaw(intern); + /// + /// Read the next 64 bits to make up a long (int64). + /// + /// Next long value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long ReadLong() => _reader.ReadLong(); + /// + /// Read the next 64 bits to make up an unsigned long (uint64). + /// + /// Next ulong value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong ReadULong() => _reader.ReadULong(); + /// + /// Read the next 32 bits to make up an int (int32). + /// + /// Next int value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadInt() => _reader.ReadInt(); + /// + /// Read the next 32 bits to make up an unsigned int (uint32). + /// + /// Next uint value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint ReadUInt() => _reader.ReadUInt(); + /// + /// Read the next 16 bits to make up a short (int16). + /// + /// Next short value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public short ReadShort() => _reader.ReadShort(); + /// + /// Read the next 16 bits to make up an unsigned short (int16). + /// + /// Next ushort value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort ReadUShort() => _reader.ReadUShort(); + /// + /// Read the next 8 bits to make up a float point double. + /// + /// Next double value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double ReadDouble() => _reader.ReadDouble(); + /// + /// Read the next 4 bits to make up a float point. + /// + /// Next float value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float ReadFloat() => _reader.ReadFloat(); + /// + /// Read the next 8 bits to make up a byte. + /// + /// Next byte value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte ReadByte() => _reader.ReadByte(); + /// + /// Read the next 8 bits to make up a signed byte. + /// + /// Next sbyte value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public sbyte ReadSByte() => _reader.ReadSByte(); + /// + /// Read the next 1 bit to make up a boolean. + /// + /// Next bool value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ReadBool() => _reader.ReadBool(); + /// + /// Read the next 32 bytes to make up a . + /// + /// Next uint value cast as a struct. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Serial ReadSerial() => _reader.ReadSerial(); + + /// + /// Reads the next Byte which helps determin 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
+ ///
+ /// Next Type value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Type ReadType() => _reader.ReadType(); + + /// + /// Reads the next set of bytes to fill the buffer. + /// + /// A reference span that will be filled with the next set of bytes. + /// The length of the buffer. + /// Thrown if the buffer is larger than the remaining data to read in the file. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Read(Span buffer) => _reader.Read(buffer); + + /// + /// Sets the current position of the stream to a specified value. + /// + /// The new position, relative to the parameter. + /// The reference point for the parameter. It can be one of the values of . + /// The new position in the stream, in bytes. + /// Thrown when the or the resulting position is out of the valid range. + /// Thrown if the stream does not support seeking. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long Seek(long offset, SeekOrigin origin) => _reader.Seek(offset, origin); +} diff --git a/Projects/Server/Serialization/BufferWriter.cs b/Projects/Server/Serialization/BufferWriter.cs index 2d425ba1be..d3be1f0508 100644 --- a/Projects/Server/Serialization/BufferWriter.cs +++ b/Projects/Server/Serialization/BufferWriter.cs @@ -105,13 +105,7 @@ public void Resize(int size) _buffer = newBuffer; } - public virtual void Flush() - { - // Need to avoid buffer.Length = 2, buffer * 2 is 4, but we need 8 or 16bytes, causing an exception. - // The least we need is 16bytes + Index, but we use BufferSize since it should always be big enough for a single - // non-dynamic field. - Resize(Math.Max(BufferSize, _buffer.Length * 2)); - } + public virtual void Flush() => Resize(Math.Clamp(_buffer.Length * 2, BufferSize, _buffer.Length + 1024 * 1024 * 64)); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void FlushIfNeeded(int amount) diff --git a/Projects/Server/Serialization/GenericEntityPersistence.cs b/Projects/Server/Serialization/GenericEntityPersistence.cs index 28a9696da6..54617198f4 100644 --- a/Projects/Server/Serialization/GenericEntityPersistence.cs +++ b/Projects/Server/Serialization/GenericEntityPersistence.cs @@ -14,7 +14,6 @@ *************************************************************************/ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -32,141 +31,115 @@ public interface IGenericEntityPersistence public void DeserializeIndexes(string savePath, Dictionary typesDb); } -public class GenericEntityPersistence : Persistence, IGenericEntityPersistence where T : class, ISerializable +public class GenericEntityPersistence : GenericPersistence, IGenericEntityPersistence where T : class, ISerializable { private static readonly ILogger logger = LogFactory.GetLogger(typeof(GenericEntityPersistence)); - private static List>[] _entities; - private long _initialIdxSize = 1024 * 256; - private long _initialBinSize = 1024 * 1024; - private readonly string _name; - private readonly uint _minSerial; - private readonly uint _maxSerial; + // Support legacy split file serialization + private static Dictionary>> _entities; + + private readonly Serial _minSerial; + private readonly Serial _maxSerial; private Serial _lastEntitySerial; private readonly Dictionary _pendingAdd = new(); private readonly Dictionary _pendingDelete = new(); - private readonly uint[] _entitiesCount = new uint[World.GetThreadWorkerCount()]; - - private readonly (MemoryMapFileWriter idxWriter, MemoryMapFileWriter binWriter)[] _writers = - new (MemoryMapFileWriter, MemoryMapFileWriter)[World.GetThreadWorkerCount()]; - public Dictionary EntitiesBySerial { get; } = new(); - public GenericEntityPersistence(string name, int priority, uint minSerial, uint maxSerial) : base(priority) + public GenericEntityPersistence(string name, int priority, uint minSerial, uint maxSerial) : this( + name, + priority, + (Serial)minSerial, + (Serial)maxSerial + ) { - _name = name; - _minSerial = minSerial; - _maxSerial = maxSerial; - _lastEntitySerial = (Serial)(minSerial - 1); - typeof(T).RegisterFindEntity(Find); } - public override void Preserialize(string savePath, ConcurrentQueue types) + public GenericEntityPersistence(string name, int priority, Serial minSerial, Serial maxSerial) : base(name, priority) { - var path = Path.Combine(savePath, _name); - PathUtility.EnsureDirectory(path); - - var threadCount = World.GetThreadWorkerCount(); - for (var i = 0; i < threadCount; i++) - { - var idxPath = Path.Combine(path, $"{_name}_{i}.idx"); - var binPath = Path.Combine(path, $"{_name}_{i}.bin"); - - _writers[i] = ( - new MemoryMapFileWriter(new FileStream(idxPath, FileMode.Create), _initialIdxSize, types), - new MemoryMapFileWriter(new FileStream(binPath, FileMode.Create), _initialBinSize, types) - ); - - _writers[i].idxWriter.Write(3); // version - _writers[i].idxWriter.Seek(4, SeekOrigin.Current); // Entity count - - _entitiesCount[i] = 0; - } + _minSerial = minSerial; + _maxSerial = maxSerial; + _lastEntitySerial = minSerial - 1; + typeof(T).RegisterFindEntity(Find); } - public override void Serialize(IGenericSerializable e, int threadIndex) + public override void WriteSnapshot(string savePath, HashSet typeSet) { - var (idx, bin) = _writers[threadIndex]; - var pos = bin.Position; - - var entity = (ISerializable)e; + var dir = Path.Combine(savePath, Name); + PathUtility.EnsureDirectory(dir); - entity.Serialize(bin); - var length = (uint)(bin.Position - pos); + var threads = World._threadWorkers; - var t = entity.GetType(); - idx.Write(t); - idx.Write(entity.Serial); - idx.Write(entity.Created.Ticks); - idx.Write(pos); - idx.Write(length); + using var binFs = new FileStream(Path.Combine(dir, $"{Name}.bin"), FileMode.Create, FileAccess.Write, FileShare.None); + using var idxFs = new FileStream(Path.Combine(dir, $"{Name}.idx"), FileMode.Create); + using var idx = new MemoryMapFileWriter(idxFs, 1024 * 1024, typeSet); // 1MB - _entitiesCount[threadIndex]++; - } + var binPosition = 0L; - public override void WriteSnapshot() - { - var wroteFile = false; - string folderPath = null; - for (int i = 0; i < _writers.Length; i++) + // Support for non-entity generic serialization. + if (SerializedLength > 0) { - var (idxWriter, binWriter) = _writers[i]; - - var binBytesWritten = binWriter.Position; - - // Write the entity count - var pos = idxWriter.Position; - idxWriter.Seek(4, SeekOrigin.Begin); - idxWriter.Write(_entitiesCount[i]); - idxWriter.Seek(pos, SeekOrigin.Begin); - - var idxFs = idxWriter.FileStream; - var idxFilePath = idxFs.Name; - var binFs = binWriter.FileStream; - var binFilePath = binFs.Name; - - if (_initialIdxSize < idxFs.Position) + try { - _initialIdxSize = idxFs.Position; + binFs.Write(threads[SerializedThread].GetHeap(SerializedPosition, SerializedLength)); } - - if (_initialBinSize < binFs.Position) + catch (Exception error) { - _initialBinSize = binFs.Position; + logger.Error( + error, + "Error writing entity: (Thread: {Thread} - {Start} {Length})", + SerializedThread, + SerializedPosition, + SerializedLength + ); } - idxWriter.Dispose(); - binWriter.Dispose(); + binPosition += SerializedLength; + } + + idx.Write(3); // Version + idx.Write(EntitiesBySerial.Values.Count); - idxFs.Dispose(); - binFs.Dispose(); + foreach (var e in EntitiesBySerial.Values) + { + var thread = e.SerializedThread; + var heapStart = e.SerializedPosition; + var heapLength = e.SerializedLength; - if (binBytesWritten > 1) + idx.Write(e.GetType()); + idx.Write(e.Serial); + idx.Write(e.Created.Ticks); + idx.Write(binPosition); + idx.Write(heapLength); + + try { - wroteFile = true; + binFs.Write(threads[thread].GetHeap(heapStart, heapLength)); } - else + catch (Exception error) { - File.Delete(idxFilePath); - File.Delete(binFilePath); - folderPath = Path.GetDirectoryName(idxFilePath); + logger.Error( + error, + "Error writing entity: {Entity} (Thread: {Thread} - {Start} {Length})", + e, + thread, + heapStart, + heapLength + ); } - } - if (!wroteFile && folderPath != null) - { - Directory.Delete(folderPath); + binPosition += heapLength; } } public override void Serialize() { - World.ResetRoundRobin(); foreach (var entity in EntitiesBySerial.Values) { - World.PushToCache((entity, this)); + World.PushToCache(entity); } + + World.PushToCache(this); } private static ConstructorInfo GetConstructorFor(string typeName, Type t, Type[] constructorTypes) @@ -205,7 +178,7 @@ private static ConstructorInfo GetConstructorFor(string typeName, Type t, Type[] */ private unsafe Dictionary ReadTypes(string savePath) { - string typesPath = Path.Combine(savePath, _name, $"{_name}.tdb"); + string typesPath = Path.Combine(savePath, Name, $"{Name}.tdb"); if (!File.Exists(typesPath)) { return null; @@ -236,59 +209,57 @@ private unsafe Dictionary ReadTypes(string savePath) public virtual void DeserializeIndexes(string savePath, Dictionary typesDb) { - string indexPath = Path.Combine(savePath, _name, $"{_name}.idx"); + string indexPath = Path.Combine(savePath, Name, $"{Name}.idx"); + + _entities ??= []; + + // Support for legacy MUO Serialization that used split files if (!File.Exists(indexPath)) { - TryDeserializeMultithreadIndexes(savePath, typesDb); + TryDeserializeSplitFileIndexes(savePath, typesDb); return; } - _entities = [InternalDeserializeIndexes(indexPath, typesDb)]; + InternalDeserializeIndexes(indexPath, typesDb, _entities[0] = []); } - private void TryDeserializeMultithreadIndexes(string savePath, Dictionary typesDb) + private void TryDeserializeSplitFileIndexes(string savePath, Dictionary typesDb) { var index = 0; - var fileList = new List(); while (true) { - var path = Path.Combine(savePath, _name, $"{_name}_{index}.idx"); + var path = Path.Combine(savePath, Name, $"{Name}_{index}.idx"); var fi = new FileInfo(path); if (!fi.Exists) { break; } - if (fi.Length != 0) + if (fi.Length == 0) { - fileList.Add(path); + continue; } + InternalDeserializeIndexes(path, typesDb, _entities[index] = []); index++; } - - _entities = new List>[fileList.Count]; - for (var i = 0; i < fileList.Count; i++) - { - _entities[i] = InternalDeserializeIndexes(fileList[i], typesDb); - } } - private unsafe List> InternalDeserializeIndexes(string filePath, Dictionary typesDb) + private unsafe void InternalDeserializeIndexes( + string filePath, Dictionary typesDb, List> entities + ) { - object[] ctorArgs = new object[1]; - List> entities = []; - using var mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open); using var accessor = mmf.CreateViewStream(); byte* ptr = null; accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); - UnmanagedDataReader dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb); + var dataReader = new UnmanagedDataReader(ptr, accessor.Length); var version = dataReader.ReadInt(); - Dictionary ctors = null; + Dictionary ctors = []; + if (version < 2) { ctors = ReadTypes(Path.GetDirectoryName(filePath)); @@ -296,15 +267,16 @@ private unsafe List> InternalDeserializeIndexes(string filePath, D if (typesDb == null && ctors == null) { - return entities; + return; } - int count = dataReader.ReadInt(); - var now = DateTime.UtcNow; + var ctorArgs = new object[1]; Type[] ctorArguments = [typeof(Serial)]; - for (int i = 0; i < count; ++i) + var count = dataReader.ReadInt(); + + for (var i = 0; i < count; ++i) { ConstructorInfo ctor; // Version 2 & 3 with SerializedTypes.db @@ -331,6 +303,7 @@ private unsafe List> InternalDeserializeIndexes(string filePath, D { dataReader.ReadLong(); // LastSerialized } + var pos = dataReader.ReadLong(); var length = dataReader.ReadInt(); @@ -344,45 +317,39 @@ private unsafe List> InternalDeserializeIndexes(string filePath, D if (ctor.Invoke(ctorArgs) is T entity) { entity.Created = created; - entities.Add(new EntitySpan(entity, pos, (int)length)); + entities.Add(new EntitySpan(entity, pos, length)); EntitiesBySerial[serial] = entity; } } accessor.SafeMemoryMappedViewHandle.ReleasePointer(); - entities.TrimExcess(); if (EntitiesBySerial.Count > 0) { _lastEntitySerial = EntitiesBySerial.Keys.Max(); } - - return entities; } public override void Deserialize(string savePath, Dictionary typesDb) { - string dataPath = Path.Combine(savePath, _name, $"{_name}.bin"); + string dataPath = Path.Combine(savePath, Name, $"{Name}.bin"); var fi = new FileInfo(dataPath); if (!fi.Exists) { TryDeserializeMultithread(savePath, typesDb); } - else + else if (fi.Length > 0) { - if (fi.Length == 0) - { - return; - } - InternalDeserialize(dataPath, 0, typesDb); } + _entities.Clear(); + _entities.TrimExcess(); _entities = null; } - private static unsafe void InternalDeserialize(string filePath, int index, Dictionary typesDb) + private unsafe void InternalDeserialize(string filePath, int index, Dictionary typesDb) { using var mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open); using var accessor = mmf.CreateViewStream(); @@ -390,6 +357,9 @@ private static unsafe void InternalDeserialize(string filePath, int index, Dicti byte* ptr = null; accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); UnmanagedDataReader dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb); + + Deserialize(dataReader); + var deleteAllFailures = false; foreach (var entry in _entities[index]) @@ -460,15 +430,25 @@ private void TryDeserializeMultithread(string savePath, Dictionary max) { - last = (Serial)_minSerial; + last = min; } - if (FindEntity(last) == null) + if (FindEntity((Serial)last) == null) { - return _lastEntitySerial = last; + return _lastEntitySerial = (Serial)last; } } - OutOfMemory($"No serials left to allocate for {_name}"); + OutOfMemory($"No serials left to allocate for {Name}"); return Serial.MinusOne; } } @@ -530,17 +511,21 @@ public void AddEntity(T entity) goto case WorldState.Loading; } case WorldState.Loading: + case WorldState.WritingSave: { if (_pendingDelete.Remove(entity.Serial)) { - logger.Warning("Deleted then added {Entity} during {WorldState} state.", entity.GetType().Name, worldState.ToString()); + logger.Warning( + "Deleted then added {Entity} during {WorldState} state.", + entity.GetType().Name, + worldState.ToString() + ); } _pendingAdd[entity.Serial] = entity; break; } case WorldState.PendingSave: - case WorldState.WritingSave: case WorldState.Running: { ref var entityEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(EntitiesBySerial, entity.Serial, out bool exists); @@ -591,13 +576,13 @@ public void RemoveEntity(T entity) goto case WorldState.Loading; } case WorldState.Loading: + case WorldState.WritingSave: { _pendingAdd.Remove(entity.Serial); _pendingDelete[entity.Serial] = entity; break; } case WorldState.PendingSave: - case WorldState.WritingSave: case WorldState.Running: { EntitiesBySerial.Remove(entity.Serial); @@ -613,8 +598,6 @@ private void ProcessSafetyQueues() AddEntity(entity); } - _pendingAdd.Clear(); - foreach (var entity in _pendingDelete.Values) { if (_pendingAdd.ContainsKey(entity.Serial)) @@ -625,10 +608,11 @@ private void ProcessSafetyQueues() RemoveEntity(entity); } + _pendingAdd.Clear(); _pendingDelete.Clear(); } - private void AppendSafetyLog(string action, ISerializable entity) + private static void AppendSafetyLog(string action, ISerializable entity) { var message = $"Warning: Attempted to {{Action}} {{Entity}} during world save.{Environment.NewLine}This action could cause inconsistent state.{Environment.NewLine}It is strongly advised that the offending scripts be corrected."; @@ -638,7 +622,7 @@ private void AppendSafetyLog(string action, ISerializable entity) try { using var op = new StreamWriter("world-save-errors.log", true); - op.WriteLine("{0}\t{1}", DateTime.UtcNow, message); + op.WriteLine($"{DateTime.UtcNow}\t{message}"); op.WriteLine(new StackTrace(2).ToString()); op.WriteLine(); } @@ -649,17 +633,9 @@ private void AppendSafetyLog(string action, ISerializable entity) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T Find(Serial serial) => FindEntity(serial, false, false); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T Find(Serial serial, bool returnDeleted) => FindEntity(serial, returnDeleted, false); + public T Find(Serial serial, bool returnDeleted = false) => FindEntity(serial, returnDeleted); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T Find(Serial serial, bool returnDeleted, bool returnPending) => FindEntity(serial, returnDeleted, returnPending); - - public R FindEntity(Serial serial) where R : class, T => FindEntity(serial, false, false); - - public R FindEntity(Serial serial, bool returnDeleted, bool returnPending) where R : class, T + public R FindEntity(Serial serial, bool returnDeleted = false) where R : class, T { switch (World.WorldState) { @@ -669,14 +645,14 @@ public R FindEntity(Serial serial, bool returnDeleted, bool returnPending) wh } case WorldState.Loading: case WorldState.Saving: + case WorldState.WritingSave: { - if (returnDeleted && returnPending && _pendingDelete.TryGetValue(serial, out var entity)) + if (returnDeleted && _pendingDelete.TryGetValue(serial, out var entity)) { return entity as R; } - if (returnPending && _pendingAdd.TryGetValue(serial, out entity) || - EntitiesBySerial.TryGetValue(serial, out entity)) + if (_pendingAdd.TryGetValue(serial, out entity) || EntitiesBySerial.TryGetValue(serial, out entity)) { return entity as R; } @@ -684,7 +660,6 @@ public R FindEntity(Serial serial, bool returnDeleted, bool returnPending) wh return null; } case WorldState.PendingSave: - case WorldState.WritingSave: case WorldState.Running: { return EntitiesBySerial.TryGetValue(serial, out var entity) ? entity as R : null; diff --git a/Projects/Server/Serialization/GenericPersistence.cs b/Projects/Server/Serialization/GenericPersistence.cs index a584b76a3e..a89e191411 100644 --- a/Projects/Server/Serialization/GenericPersistence.cs +++ b/Projects/Server/Serialization/GenericPersistence.cs @@ -14,65 +14,105 @@ *************************************************************************/ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.IO.MemoryMappedFiles; namespace Server; public abstract class GenericPersistence : Persistence, IGenericSerializable { - private long _initialSize = 1024 * 1024; - private MemoryMapFileWriter _fileToSave; - public string Name { get; } + public string SaveFilePath { get; protected set; } // "/.bin" - public GenericPersistence(string name, int priority) : base(priority) => Name = name; + public byte SerializedThread { get; set; } + public int SerializedPosition { get; set; } + public int SerializedLength { get; set; } - public override void Preserialize(string savePath, ConcurrentQueue types) + public GenericPersistence(string name, int priority) : base(priority) { - var path = Path.Combine(savePath, Name); - var filePath = Path.Combine(path, $"{Name}.bin"); - PathUtility.EnsureDirectory(path); - - _fileToSave = new MemoryMapFileWriter(new FileStream(filePath, FileMode.Create), _initialSize, types); + Name = name; + SaveFilePath = Path.Combine(Name, $"{Name}.bin"); } public override void Serialize() { - World.ResetRoundRobin(); - World.PushToCache((this, this)); + World.PushToCache(this); } - public override void WriteSnapshot() + public override void WriteSnapshot(string savePath, HashSet typeSet) { - string folderPath = null; - using (var fs = _fileToSave.FileStream) + if (SerializedLength == 0) { - if (fs.Position > _initialSize) - { - _initialSize = fs.Position; - } + return; + } - _fileToSave.Dispose(); - if (_fileToSave.Position == 0) - { - folderPath = Path.GetDirectoryName(fs.Name); - } + var file = Path.Combine(savePath, SaveFilePath); + var dir = Path.GetDirectoryName(file); + PathUtility.EnsureDirectory(dir); + + var threads = World._threadWorkers; + + using var binFs = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None); + + var thread = SerializedThread; + var heapStart = SerializedPosition; + var heapLength = SerializedLength; + + binFs.Write(threads[thread].GetHeap(heapStart, heapLength)); + } + + public override unsafe void Deserialize(string savePath, Dictionary typesDb) + { + // Assume savePath has the Core.BaseDirectory already prepended + var dataPath = Path.GetFullPath(SaveFilePath, savePath); + var file = new FileInfo(dataPath); + + if (!file.Exists || file.Length <= 0) + { + return; } - if (folderPath != null) + var fileLength = file.Length; + + string error; + + try + { + using var mmf = MemoryMappedFile.CreateFromFile(dataPath, FileMode.Open); + using var accessor = mmf.CreateViewStream(); + + byte* ptr = null; + accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); + var dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb); + Deserialize(dataReader); + + error = dataReader.Position != fileLength + ? $"Serialized {fileLength} bytes, but {dataReader.Position} bytes deserialized" + : null; + + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); + } + catch (Exception e) { - Directory.Delete(folderPath); + error = e.ToString(); } - } - public override void Serialize(IGenericSerializable e, int threadIndex) => Serialize(_fileToSave); + if (error != null) + { + Console.WriteLine($"***** Bad deserialize of {file.FullName} *****"); + Console.WriteLine(error); - public abstract void Serialize(IGenericWriter writer); + Console.Write("Skip this file and continue? (y/n): "); + var y = Console.ReadLine(); - public override void Deserialize(string savePath, Dictionary typesDb) => - AdhocPersistence.Deserialize(Path.Combine(savePath, Name, $"{Name}.bin"), Deserialize); + if (!y.InsensitiveEquals("y")) + { + throw new Exception("Deserialization failed."); + } + } + } + public abstract void Serialize(IGenericWriter writer); public abstract void Deserialize(IGenericReader reader); } diff --git a/Projects/Server/World/IGenericSerializable.cs b/Projects/Server/Serialization/IGenericSerializable.cs similarity index 90% rename from Projects/Server/World/IGenericSerializable.cs rename to Projects/Server/Serialization/IGenericSerializable.cs index 02e6487afd..ecc54042de 100644 --- a/Projects/Server/World/IGenericSerializable.cs +++ b/Projects/Server/Serialization/IGenericSerializable.cs @@ -17,5 +17,9 @@ namespace Server; public interface IGenericSerializable { + byte SerializedThread { get; set; } + int SerializedPosition { get; set; } + int SerializedLength { get; set; } + void Serialize(IGenericWriter writer); } diff --git a/Projects/Server/Serialization/MemoryMapFileWriter.cs b/Projects/Server/Serialization/MemoryMapFileWriter.cs index e5de0849fe..67de0a4ab6 100644 --- a/Projects/Server/Serialization/MemoryMapFileWriter.cs +++ b/Projects/Server/Serialization/MemoryMapFileWriter.cs @@ -15,7 +15,7 @@ using System; using System.Buffers.Binary; -using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.IO.MemoryMappedFiles; using System.Runtime.CompilerServices; @@ -29,7 +29,7 @@ public unsafe class MemoryMapFileWriter : IGenericWriter, IDisposable { private readonly Encoding _encoding; - private readonly ConcurrentQueue _types; + private readonly HashSet _types; private readonly FileStream _fileStream; private MemoryMappedFile _mmf; private MemoryMappedViewAccessor _accessor; @@ -37,7 +37,7 @@ public unsafe class MemoryMapFileWriter : IGenericWriter, IDisposable private long _position; private long _size; - public MemoryMapFileWriter(FileStream fileStream, long initialSize, ConcurrentQueue types = null) + public MemoryMapFileWriter(FileStream fileStream, long initialSize, HashSet types = null) { _types = types; _fileStream = fileStream; @@ -244,7 +244,7 @@ public void Write(Type type) { Write((byte)0x2); // xxHash3 64bit Write(AssemblyHandler.GetTypeHash(type)); - _types.Enqueue(type); + _types.Add(type); } } diff --git a/Projects/Server/Serialization/Persistence.cs b/Projects/Server/Serialization/Persistence.cs index d98f6551c7..3c41c564e6 100644 --- a/Projects/Server/Serialization/Persistence.cs +++ b/Projects/Server/Serialization/Persistence.cs @@ -14,7 +14,6 @@ *************************************************************************/ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.MemoryMappedFiles; @@ -53,7 +52,7 @@ public static void Load(string path) } } - private unsafe static Dictionary LoadTypes(string path) + private static unsafe Dictionary LoadTypes(string path) { var db = new Dictionary(); @@ -85,32 +84,31 @@ private unsafe static Dictionary LoadTypes(string path) } // Note: This is strictly on a background thread - internal static void PreSerializeAll(string path, ConcurrentQueue types) + internal static void WriteSnapshotAll(string path, HashSet typeSet) { foreach (var p in _registry) { - p.Preserialize(path, types); + p.WriteSnapshot(path, typeSet); } - } - private static readonly HashSet _typesSet = []; + WriteSerializedTypesSnapshot(path, typeSet); + } - // Note: This is strictly on a background thread - internal static void WriteSnapshotAll(string path, ConcurrentQueue types) + public static void WriteSerializedTypesSnapshot(string path, HashSet types) { - foreach (var p in _registry) - { - p.WriteSnapshot(); - } + string typesPath = Path.Combine(path, "SerializedTypes.db"); + using var fs = new FileStream(typesPath, FileMode.Create); + using var writer = new MemoryMapFileWriter(fs, 1024 * 1024 * 4); + + writer.Write(0); // version + writer.Write(types.Count); - // Dedupe the queue. foreach (var type in types) { - _typesSet.Add(type); + var fullName = type.FullName; + writer.Write(HashUtility.ComputeHash64(fullName)); + writer.WriteStringRaw(fullName); } - - WriteSerializedTypesSnapshot(path, _typesSet); - _typesSet.Clear(); } internal static void SerializeAll() @@ -137,32 +135,8 @@ internal static void PostDeserializeAll() } } - public static void WriteSerializedTypesSnapshot(string path, HashSet types) - { - string typesPath = Path.Combine(path, "SerializedTypes.db"); - using var fs = new FileStream(typesPath, FileMode.Create); - using var writer = new MemoryMapFileWriter(fs, 1024 * 1024 * 4); - - writer.Write(0); // version - writer.Write(types.Count); - - foreach (var type in types) - { - var fullName = type.FullName; - writer.Write(HashUtility.ComputeHash64(fullName)); - writer.WriteStringRaw(fullName); - } - } - - // Open file streams, MMFs, prepare data structures - // Note: This should only be run on a background thread - public abstract void Preserialize(string savePath, ConcurrentQueue types); - - // Note: This should only be run on a background thread - public abstract void Serialize(IGenericSerializable e, int threadIndex); - // Note: This should only be run on a background thread - public abstract void WriteSnapshot(); + public abstract void WriteSnapshot(string savePath, HashSet typeSet); public abstract void Serialize(); diff --git a/Projects/Server/Serialization/SerializationExtensions.cs b/Projects/Server/Serialization/SerializationExtensions.cs index 0e89cbca36..198837dc0a 100644 --- a/Projects/Server/Serialization/SerializationExtensions.cs +++ b/Projects/Server/Serialization/SerializationExtensions.cs @@ -21,10 +21,10 @@ namespace Server; public static class SerializationExtensions { - private static readonly Dictionary> _directFinderTable = new(); - private static readonly Dictionary> _searchTable = new(); + private static readonly Dictionary> _directFinderTable = new(); + private static readonly Dictionary> _searchTable = new(); - public static void RegisterFindEntity(this Type type, Func func) + public static void RegisterFindEntity(this Type type, Func func) { _searchTable[type] = func; } @@ -47,12 +47,12 @@ public static T ReadEntity(this IGenericReader reader) where T : class, ISeri if (typeof(IEntity).IsAssignableFrom(typeT)) { - return World.FindEntity(serial, returnPending: false) as T; + return World.FindEntity(serial) as T; } if (_directFinderTable.TryGetValue(typeT, out var finder)) { - return finder(serial, false, false) as T; + return finder(serial, false) as T; } Type type = null; @@ -87,7 +87,7 @@ public static T ReadEntity(this IGenericReader reader) where T : class, ISeri finder = _searchTable[type]; _directFinderTable[type] = finder; - return finder(serial, false, false) as T; + return finder(serial, false) as T; } public static List ReadEntityList( diff --git a/Projects/Server/Serialization/SerializationThreadWorker.cs b/Projects/Server/Serialization/SerializationThreadWorker.cs new file mode 100644 index 0000000000..d01dca4704 --- /dev/null +++ b/Projects/Server/Serialization/SerializationThreadWorker.cs @@ -0,0 +1,113 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2024 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializationThreadWorker.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.Runtime.CompilerServices; +using System.Threading; + +namespace Server; + +public class SerializationThreadWorker +{ + private const int MinHeapSize = 1024 * 1024; // 1MB + private readonly int _index; + private readonly Thread _thread; + private readonly AutoResetEvent _startEvent; // Main thread tells the thread to start working + private readonly AutoResetEvent _stopEvent; // Main thread waits for the worker finish draining + private bool _pause; + private bool _exit; + private byte[] _heap; + + private readonly ConcurrentQueue _entities; + + public SerializationThreadWorker(int index) + { + _index = index; + _startEvent = new AutoResetEvent(false); + _stopEvent = new AutoResetEvent(false); + _entities = new ConcurrentQueue(); + _thread = new Thread(Execute); + _thread.Start(this); + } + + public void Wake() + { + _startEvent.Set(); + } + + public void Sleep() + { + Volatile.Write(ref _pause, true); + _stopEvent.WaitOne(); + } + + public void Exit() + { + _exit = true; + Wake(); + Sleep(); + } + + public void AllocateHeap() => _heap ??= GC.AllocateUninitializedArray(MinHeapSize); // 1MB + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Push(IGenericSerializable entity) => _entities.Enqueue(entity); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan GetHeap(int start, int length) => _heap.AsSpan(start, length); + + private static void Execute(object obj) + { + var worker = (SerializationThreadWorker)obj; + var threadIndex = (byte)worker._index; + + var queue = worker._entities; + var serializedTypes = World.SerializedTypes; + + while (worker._startEvent.WaitOne()) + { + var writer = new BufferWriter(worker._heap, true, serializedTypes); + + while (true) + { + var pauseRequested = Volatile.Read(ref worker._pause); + if (queue.TryDequeue(out var e)) + { + e.SerializedThread = threadIndex; + var start = e.SerializedPosition = (int)writer.Position; + e.Serialize(writer); + e.SerializedLength = (int)(writer.Position - start); + } + else if (pauseRequested) // Break when finished + { + break; + } + } + + worker._heap = writer.Buffer; + + writer.Close(); + + worker._stopEvent.Set(); // Allow the main thread to continue now that we are finished + worker._pause = false; + + if (Core.Closing || worker._exit) + { + return; + } + } + } +} diff --git a/Projects/Server/Serialization/UnmanagedDataReader.cs b/Projects/Server/Serialization/UnmanagedDataReader.cs index ed9675abb5..9babb86fda 100644 --- a/Projects/Server/Serialization/UnmanagedDataReader.cs +++ b/Projects/Server/Serialization/UnmanagedDataReader.cs @@ -25,19 +25,31 @@ namespace Server; +/// +/// Read bits of data raw from a serialized file using Little-endian. +/// public unsafe class UnmanagedDataReader : IGenericReader { private static readonly ILogger logger = LogFactory.GetLogger(typeof(UnmanagedDataReader)); private readonly byte* _ptr; - private long _position; private readonly long _size; private readonly Dictionary _typesDb; private readonly Encoding _encoding; - public long Position => _position; - + /// + /// How many bits deep into the file is the reader at currently. + /// + public long Position { get; private set; } + + /// + /// Read bits of data raw from a serialized file using Little-endian. + /// + /// The starting address for reading bits. + /// The total size of memory to be read. + /// The custom type dictionary. Will throw an error if left null ReadType is called. + /// by default. public UnmanagedDataReader(byte* ptr, long size, Dictionary typesDb = null, Encoding encoding = null) { _encoding = encoding ?? TextEncoding.UTF8; @@ -46,9 +58,19 @@ public UnmanagedDataReader(byte* ptr, long size, Dictionary types _size = size; } + /// + /// Reads the next bit as a bool. If that bit is true, return an else null. + /// + /// + /// Next string value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadString(bool intern = false) => ReadBool() ? ReadStringRaw(intern) : null; + /// + /// Returns the next set of bits that make up a string. + /// + /// + /// Next string value. public string ReadStringRaw(bool intern = false) { // ReadEncodedInt @@ -57,7 +79,7 @@ public string ReadStringRaw(bool intern = false) do { - b = *(_ptr + _position++); + b = *(_ptr + Position++); length |= (b & 0x7F) << shift; shift += 7; } @@ -68,95 +90,153 @@ public string ReadStringRaw(bool intern = false) return "".Intern(); } - var str = TextEncoding.GetString(new ReadOnlySpan(_ptr + _position, length), _encoding); - _position += length; + var str = TextEncoding.GetString(new ReadOnlySpan(_ptr + Position, length), _encoding); + Position += length; return intern ? str.Intern() : str; } + /// + /// Read the next 64 bits to make up a long (int64). + /// + /// Next long value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public long ReadLong() { - var v = BinaryPrimitives.ReadInt64LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(long))); - _position += sizeof(long); + var v = BinaryPrimitives.ReadInt64LittleEndian(new ReadOnlySpan(_ptr + Position, sizeof(long))); + Position += sizeof(long); return v; } + /// + /// Read the next 64 bits to make up an unsigned long (uint64). + /// + /// Next ulong value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ulong ReadULong() { - var v = BinaryPrimitives.ReadUInt64LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(ulong))); - _position += sizeof(ulong); + var v = BinaryPrimitives.ReadUInt64LittleEndian(new ReadOnlySpan(_ptr + Position, sizeof(ulong))); + Position += sizeof(ulong); return v; } + /// + /// Read the next 32 bits to make up an int (int32). + /// + /// Next int value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public int ReadInt() { - var v = BinaryPrimitives.ReadInt32LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(int))); - _position += sizeof(int); + var v = BinaryPrimitives.ReadInt32LittleEndian(new ReadOnlySpan(_ptr + Position, sizeof(int))); + Position += sizeof(int); return v; } + /// + /// Read the next 32 bits to make up an unsigned int (uint32). + /// + /// Next uint value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint ReadUInt() { - var v = BinaryPrimitives.ReadUInt32LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(uint))); - _position += sizeof(uint); + var v = BinaryPrimitives.ReadUInt32LittleEndian(new ReadOnlySpan(_ptr + Position, sizeof(uint))); + Position += sizeof(uint); return v; } + /// + /// Read the next 16 bits to make up a short (int16). + /// + /// Next short value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public short ReadShort() { - var v = BinaryPrimitives.ReadInt16LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(short))); - _position += sizeof(short); + var v = BinaryPrimitives.ReadInt16LittleEndian(new ReadOnlySpan(_ptr + Position, sizeof(short))); + Position += sizeof(short); return v; } + /// + /// Read the next 16 bits to make up an unsigned short (int16). + /// + /// Next ushort value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort ReadUShort() { - var v = BinaryPrimitives.ReadUInt16LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(ushort))); - _position += sizeof(ushort); + var v = BinaryPrimitives.ReadUInt16LittleEndian(new ReadOnlySpan(_ptr + Position, sizeof(ushort))); + Position += sizeof(ushort); return v; } + /// + /// Read the next 8 bits to make up a float point double. + /// + /// Next double value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public double ReadDouble() { - var v = BinaryPrimitives.ReadDoubleLittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(double))); - _position += sizeof(double); + var v = BinaryPrimitives.ReadDoubleLittleEndian(new ReadOnlySpan(_ptr + Position, sizeof(double))); + Position += sizeof(double); return v; } + /// + /// Read the next 4 bits to make up a float point. + /// + /// Next float value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public float ReadFloat() { - var v = BinaryPrimitives.ReadSingleLittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(float))); - _position += sizeof(float); + var v = BinaryPrimitives.ReadSingleLittleEndian(new ReadOnlySpan(_ptr + Position, sizeof(float))); + Position += sizeof(float); return v; } + /// + /// Read the next 8 bits to make up a byte. + /// + /// Next byte value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public byte ReadByte() => *(_ptr + _position++); - + public byte ReadByte() => *(_ptr + Position++); + /// + /// Read the next 8 bits to make up a signed byte. + /// + /// Next sbyte value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public sbyte ReadSByte() => (sbyte)ReadByte(); - + /// + /// Read the next Byte and return true if it's value returns zero. + /// + /// Next bool value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ReadBool() => ReadByte() != 0; + /// + /// Read the next 32 bytes to make up a . + /// + /// Next uint value cast as a struct. [MethodImpl(MethodImplOptions.AggressiveInlining)] public Serial ReadSerial() => (Serial)ReadUInt(); + /// + /// Reads the next Byte which helps determin 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
+ ///
+ /// Next Type value public Type ReadType() => ReadByte() switch { - 0 => null, 1 => AssemblyHandler.FindTypeByFullName(ReadStringRaw()), // Backward compatibility - 2 => ReadTypeByHash() + 2 => ReadTypeByHash(), + _ => null, }; + /// + /// Reads the next to create a hash and convert that into a Type using the . + /// Will log an if typesDb is null or typesDb doesn't contain the hash and return null + /// + /// Next Type value public Type ReadTypeByHash() { var hash = ReadULong(); @@ -205,19 +285,33 @@ public Type ReadTypeByHash() return t; } + /// + /// Reads the next set of bytes to fill the buffer. + /// + /// A reference span that will be filled with the next set of bytes. + /// The length of the buffer. + /// Thrown if the buffer is larger than the remaining data to read in the file. public int Read(Span buffer) { var length = buffer.Length; - if (length > _size - _position) + if (length > _size - Position) { throw new OutOfMemoryException(); } - new ReadOnlySpan(_ptr + _position, length).CopyTo(buffer); - _position += length; + new ReadOnlySpan(_ptr + Position, length).CopyTo(buffer); + Position += length; return length; } + /// + /// Sets the current position of the stream to a specified value. + /// + /// The new position, relative to the parameter. + /// The reference point for the parameter. It can be one of the values of . + /// The new position in the stream, in bytes. + /// Thrown when the or the resulting position is out of the valid range. + /// Thrown if the stream does not support seeking. public virtual long Seek(long offset, SeekOrigin origin) { Debug.Assert( @@ -229,18 +323,16 @@ public virtual long Seek(long offset, SeekOrigin origin) "Attempting to seek to an invalid position using SeekOrigin.Begin" ); Debug.Assert( - origin != SeekOrigin.Current || _position + offset >= 0 && _position + offset < _size, + origin != SeekOrigin.Current || Position + offset >= 0 && Position + offset < _size, "Attempting to seek to an invalid position using SeekOrigin.Current" ); - var position = Math.Max(0L, origin switch + Position = Math.Max(0L, origin switch { - SeekOrigin.Current => _position + offset, + SeekOrigin.Current => Position + offset, SeekOrigin.End => _size + offset, _ => offset // Begin }); - - _position = position; - return _position; + return Position; } } diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index fc974e96c2..a47b3fb1a6 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -33,7 +33,7 @@ - + diff --git a/Projects/Server/Timer/Timer.TimerWheel.cs b/Projects/Server/Timer/Timer.TimerWheel.cs index 87fbad7062..ad4630a22f 100644 --- a/Projects/Server/Timer/Timer.TimerWheel.cs +++ b/Projects/Server/Timer/Timer.TimerWheel.cs @@ -136,9 +136,6 @@ private static void Execute(Timer timer) { var finished = timer.Count != 0 && timer.Index + 1 >= timer.Count; - var prof = timer.GetProfile(); - prof?.Start(); - // Stop the timer from running so that way if Start() is called in OnTick, the timer will be started. if (finished) { @@ -149,7 +146,6 @@ private static void Execute(Timer timer) var version = timer.Version; timer.OnTick(); - prof?.Finish(); // Starting doesn't change the timer version, so we need to check if it's finished and if it's still running. if (timer.Version != version || finished && timer.Running) diff --git a/Projects/Server/Timer/Timer.cs b/Projects/Server/Timer/Timer.cs index 69b30ba9cb..536faedf9a 100644 --- a/Projects/Server/Timer/Timer.cs +++ b/Projects/Server/Timer/Timer.cs @@ -15,7 +15,6 @@ using System; using System.Diagnostics; -using Server.Diagnostics; using Server.Logging; namespace Server; @@ -54,13 +53,6 @@ protected void Init(TimeSpan delay, TimeSpan interval, int count) Next = Core.Now + Delay; _ring = -1; _slot = -1; - - var prof = GetProfile(); - - if (prof != null) - { - prof.Created++; - } } protected int Version { get; set; } // Used to determine if a timer was altered and we should abandon it. @@ -73,8 +65,6 @@ protected void Init(TimeSpan delay, TimeSpan interval, int count) public int RemainingCount => Count == 0 ? int.MaxValue : Count - Index; public bool Running { get; private set; } - public TimerProfile GetProfile() => !Core.Profiling ? null : TimerProfile.Acquire(ToString() ?? "null"); - public override string ToString() => GetType().FullName; public Timer Start() @@ -111,13 +101,6 @@ public Timer Start() Running = true; AddTimer(this, (long)Delay.TotalMilliseconds); - var prof = GetProfile(); - - if (prof != null) - { - prof.Started++; - } - return this; } @@ -174,12 +157,6 @@ private void InternalStop() { _executingRings[_ring] = _nextTimer; } - - var prof = GetProfile(); - if (prof != null) - { - prof.Stopped++; - } } protected virtual void OnTick() diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 8eff6002dc..61545077d5 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -17,7 +17,7 @@ namespace Server; -public static class Utility +public static partial class Utility { private static Dictionary _ipAddressTable; @@ -237,6 +237,8 @@ public static PooledArraySpanFormattable FixHtmlFormattable(this ReadOnlySpan.Shared.Rent(str.Length); var span = chars.AsSpan(0, str.Length); + str.CopyTo(span); + var formattable = new PooledArraySpanFormattable(chars, str.Length); if (!str.IsNullOrWhiteSpace()) @@ -1458,4 +1460,12 @@ public static int C32216(this int c32) return (r << 10) | (g << 5) | b; } + + public static void AddOrUpdate(this ConditionalWeakTable table, TKey key, TValue value) + where TKey : class + where TValue : class + { + table.Remove(key); + table.Add(key, value); + } } diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index 32659f7512..234c76d0b5 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -45,7 +45,7 @@ public static class World private static readonly GenericEntityPersistence _guildPersistence = new("Guilds", 3, 1, 0x7FFFFFFF); private static int _threadId; - private static readonly SerializationThreadWorker[] _threadWorkers = new SerializationThreadWorker[Math.Max(Environment.ProcessorCount - 1, 1)]; + internal static SerializationThreadWorker[] _threadWorkers; private static readonly ManualResetEvent _diskWriteHandle = new(true); private static readonly ConcurrentQueue _decayQueue = new(); @@ -88,6 +88,7 @@ public static Serial NewVirtual public static Dictionary Mobiles => _mobilePersistence.EntitiesBySerial; public static Dictionary Guilds => _guildPersistence.EntitiesBySerial; + public static bool UseMultiThreadedSaves { get; private set; } public static string SavePath { get; private set; } public static WorldState WorldState { get; private set; } public static bool Saving => WorldState == WorldState.Saving; @@ -101,13 +102,12 @@ public static void Configure() var savePath = ServerConfiguration.GetOrUpdateSetting("world.savePath", "Saves"); SavePath = PathUtility.GetFullPath(savePath); + + UseMultiThreadedSaves = ServerConfiguration.GetOrUpdateSetting("world.useMultithreadedSaves", true); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WaitForWriteCompletion() - { - _diskWriteHandle.WaitOne(); - } + public static void WaitForWriteCompletion() => _diskWriteHandle.WaitOne(); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void EnqueueForDecay(Item item) @@ -207,6 +207,9 @@ public static void Load() ); // Create the serialization threads. + var threadCount = UseMultiThreadedSaves ? Math.Max(Environment.ProcessorCount - 1, 1) : 1; + _threadWorkers = new SerializationThreadWorker[threadCount]; + for (var i = 0; i < _threadWorkers.Length; i++) { _threadWorkers[i] = new SerializationThreadWorker(i); @@ -267,17 +270,26 @@ public static void Save() return; } + WaitForWriteCompletion(); // Blocks Save until current disk flush is done. + _diskWriteHandle.Reset(); + WorldState = WorldState.PendingSave; ThreadPool.QueueUserWorkItem(Preserialize); } - internal static void Preserialize(object state) + private static void Preserialize(object state) { var tempPath = PathUtility.EnsureRandomPath(_tempSavePath); try { - Persistence.PreSerializeAll(tempPath, SerializedTypes); + // Allocate the heaps for the GC + foreach (var worker in _threadWorkers) + { + worker.AllocateHeap(); + } + + WakeSerializationThreads(); Core.RequestSnapshot(tempPath); } catch (Exception ex) @@ -296,9 +308,7 @@ internal static void Snapshot(string snapshotPath) return; } - WaitForWriteCompletion(); // Blocks Save until current disk flush is done. - - _diskWriteHandle.Reset(); + NetState.FlushAll(); WorldState = WorldState.Saving; @@ -314,7 +324,6 @@ internal static void Snapshot(string snapshotPath) { _serializationStart = Core.Now; - WakeSerializationThreads(); Persistence.SerializeAll(); PauseSerializationThreads(); EventSink.InvokeWorldSave(); @@ -324,9 +333,8 @@ internal static void Snapshot(string snapshotPath) exception = ex; } - WorldState = WorldState.PendingSave; + WorldState = WorldState.WritingSave; ThreadPool.QueueUserWorkItem(WriteFiles, snapshotPath); - Persistence.PostWorldSaveAll(); // Process safety queues watch.Stop(); if (exception == null) @@ -345,6 +353,8 @@ internal static void Snapshot(string snapshotPath) } } + private static readonly HashSet _typesSet = []; + private static void WriteFiles(object state) { var snapshotPath = (string)state; @@ -352,7 +362,16 @@ private static void WriteFiles(object state) { var watch = Stopwatch.StartNew(); logger.Information("Writing world save snapshot"); - Persistence.WriteSnapshotAll(snapshotPath, SerializedTypes); + + // Dedupe the types + while (SerializedTypes.TryDequeue(out var type)) + { + _typesSet.Add(type); + } + + Persistence.WriteSnapshotAll(snapshotPath, _typesSet); + + _typesSet.Clear(); try { @@ -380,7 +399,13 @@ private static void WriteFiles(object state) SerializedTypes.Clear(); _diskWriteHandle.Set(); + Core.LoopContext.Post(FinishWorldSave); + } + + private static void FinishWorldSave() + { WorldState = WorldState.Running; + Persistence.PostWorldSaveAll(); // Process decay and safety queues } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -408,9 +433,9 @@ internal static void PauseSerializationThreads() internal static void ResetRoundRobin() => _threadId = 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void PushToCache((IGenericSerializable e, Persistence p) ep) + internal static void PushToCache(IGenericSerializable e) { - _threadWorkers[_threadId++].Push(ep); + _threadWorkers[_threadId++].Push(e); if (_threadId == _threadWorkers.Length) { _threadId = 0; @@ -474,20 +499,20 @@ public static void RemoveEntity(IEntity entity) // Legacy: Only used for retrieving Items and Mobiles. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEntity FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = false) => - FindEntity(serial, returnDeleted, returnPending); + public static IEntity FindEntity(Serial serial, bool returnDeleted = false) => + FindEntity(serial, returnDeleted); // Legacy: Only used for retrieving Items and Mobiles. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = false) + public static T FindEntity(Serial serial, bool returnDeleted = false) where T : class, IEntity { if (serial.IsItem) { - return _itemPersistence.Find(serial, returnDeleted, returnPending) as T; + return _itemPersistence.Find(serial, returnDeleted) as T; } - return _mobilePersistence.Find(serial, returnDeleted, returnPending) as T; + return _mobilePersistence.Find(serial, returnDeleted) as T; } private class ItemPersistence : GenericEntityPersistence @@ -520,7 +545,7 @@ public override void Serialize() EnqueueForDecay(item); } - PushToCache((item, this)); + PushToCache(item); } } @@ -551,78 +576,4 @@ public override void PostDeserialize() } } } - - private class SerializationThreadWorker - { - private readonly int _index; - private readonly Thread _thread; - private readonly AutoResetEvent _startEvent; // Main thread tells the thread to start working - private readonly AutoResetEvent _stopEvent; // Main thread waits for the worker finish draining - private bool _pause; - private bool _exit; - private readonly ConcurrentQueue<(IGenericSerializable, Persistence)> _entities; - - public SerializationThreadWorker(int index) - { - _index = index; - _startEvent = new AutoResetEvent(false); - _stopEvent = new AutoResetEvent(false); - _entities = new ConcurrentQueue<(IGenericSerializable, Persistence)>(); - _thread = new Thread(Execute); - _thread.Start(this); - } - - public void Wake() - { - _startEvent.Set(); - } - - public void Sleep() - { - Volatile.Write(ref _pause, true); - _stopEvent.WaitOne(); - } - - public void Exit() - { - _exit = true; - Wake(); - Sleep(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Push((IGenericSerializable e, Persistence p) ep) => _entities.Enqueue(ep); - - private static void Execute(object obj) - { - SerializationThreadWorker worker = (SerializationThreadWorker)obj; - - var reader = worker._entities; - - while (worker._startEvent.WaitOne()) - { - while (true) - { - bool pauseRequested = Volatile.Read(ref worker._pause); - if (reader.TryDequeue(out var ep)) - { - var (e, p) = ep; - p.Serialize(e, worker._index); - } - else if (pauseRequested) // Break when finished - { - break; - } - } - - worker._stopEvent.Set(); // Allow the main thread to continue now that we are finished - worker._pause = false; - - if (Core.Closing || worker._exit) - { - return; - } - } - } - } } diff --git a/Projects/UOContent.Tests/UOContent.Tests.csproj b/Projects/UOContent.Tests/UOContent.Tests.csproj index 2ff1f5880f..b2cc0b6b9c 100644 --- a/Projects/UOContent.Tests/UOContent.Tests.csproj +++ b/Projects/UOContent.Tests/UOContent.Tests.csproj @@ -4,8 +4,8 @@ Debug;Release;Analyze - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Projects/UOContent/Accounting/Account.cs b/Projects/UOContent/Accounting/Account.cs index 46c2b0071a..ac7da1abe2 100644 --- a/Projects/UOContent/Accounting/Account.cs +++ b/Projects/UOContent/Accounting/Account.cs @@ -284,6 +284,10 @@ public bool Inactive public Serial Serial { get; set; } + public byte SerializedThread { get; set; } + public int SerializedPosition { get; set; } + public int SerializedLength { get; set; } + [AfterDeserialization(false)] private void AfterDeserialization() { @@ -362,13 +366,17 @@ public void Delete() public void SetPassword(string plainPassword) { - Password = AccountSecurity.CurrentPasswordProtection.EncryptPassword(plainPassword); + var phrase = _passwordAlgorithm is PasswordProtectionAlgorithm.SHA1 or PasswordProtectionAlgorithm.SHA2 + ? $"{_username}{plainPassword}" + : plainPassword; + + Password = AccountSecurity.CurrentPasswordProtection.EncryptPassword(phrase); PasswordAlgorithm = AccountSecurity.CurrentAlgorithm; } public bool CheckPassword(string plainPassword) { - var phrase = _passwordAlgorithm == PasswordProtectionAlgorithm.SHA1 + var phrase = _passwordAlgorithm is PasswordProtectionAlgorithm.SHA1 or PasswordProtectionAlgorithm.SHA2 ? $"{_username}{plainPassword}" : plainPassword; diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index 12f9a96474..fce134355b 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Net; +using ModernUO.CodeGeneratedEvents; using Server.Accounting; +using Server.Engines.CharacterCreation; using Server.Engines.Help; using Server.Logging; using Server.Network; @@ -65,7 +67,6 @@ public static void Configure() public static void Initialize() { EventSink.AccountLogin += EventSink_AccountLogin; - EventSink.GameLogin += EventSink_GameLogin; } [Usage("Password ")] @@ -348,7 +349,8 @@ public static void EventSink_AccountLogin(AccountLoginEventArgs e) } } - public static void EventSink_GameLogin(GameLoginEventArgs e) + [OnEvent(nameof(GameServer.GameServerLoginEvent))] + public static void OnGameServerLogin(GameServer.GameLoginEventArgs e) { var un = e.Username; var pw = e.Password; diff --git a/Projects/UOContent/Commands/Profiling.cs b/Projects/UOContent/Commands/Profiling.cs deleted file mode 100644 index c1c362fa87..0000000000 --- a/Projects/UOContent/Commands/Profiling.cs +++ /dev/null @@ -1,427 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Server.Diagnostics; - -namespace Server.Commands -{ - public static class Profiling - { - public static void Configure() - { - CommandSystem.Register("DumpTimers", AccessLevel.Administrator, DumpTimers_OnCommand); - CommandSystem.Register("CountObjects", AccessLevel.Administrator, CountObjects_OnCommand); - CommandSystem.Register("ProfileWorld", AccessLevel.Administrator, ProfileWorld_OnCommand); - CommandSystem.Register("TraceInternal", AccessLevel.Administrator, TraceInternal_OnCommand); - CommandSystem.Register("TraceExpanded", AccessLevel.Administrator, TraceExpanded_OnCommand); - CommandSystem.Register("WriteProfiles", AccessLevel.Administrator, WriteProfiles_OnCommand); - CommandSystem.Register("SetProfiles", AccessLevel.Administrator, SetProfiles_OnCommand); - } - - [Usage("WriteProfiles")] - [Description("Generates a log files containing performance diagnostic information.")] - public static void WriteProfiles_OnCommand(CommandEventArgs e) - { - try - { - using var sw = new StreamWriter("profiles.log", true); - sw.WriteLine("# Dump on {0:f}", Core.Now); - sw.WriteLine($"# Core profiling for {Core.ProfileTime}"); - - sw.WriteLine("# Packet send"); - BaseProfile.WriteAll(sw, PacketSendProfile.Profiles); - sw.WriteLine(); - - sw.WriteLine("# Packet receive"); - BaseProfile.WriteAll(sw, PacketReceiveProfile.Profiles); - sw.WriteLine(); - - sw.WriteLine("# Timer"); - BaseProfile.WriteAll(sw, TimerProfile.Profiles); - sw.WriteLine(); - - sw.WriteLine("# Gump response"); - BaseProfile.WriteAll(sw, GumpProfile.Profiles); - sw.WriteLine(); - - sw.WriteLine("# Target response"); - BaseProfile.WriteAll(sw, TargetProfile.Profiles); - sw.WriteLine(); - } - catch - { - // ignored - } - } - - [Usage("SetProfiles [true | false]"), - Description("Enables, disables, or toggles the state of core packet and timer profiling.")] - public static void SetProfiles_OnCommand(CommandEventArgs e) - { - if (e.Length == 1) - { - Core.Profiling = e.GetBoolean(0); - } - else - { - Core.Profiling = !Core.Profiling; - } - - if (Core.Profiling) - { - e.Mobile.SendMessage($"Profiling has been enabled."); - } - else - { - e.Mobile.SendMessage($"Profiling has been disabled."); - } - } - - [Usage("DumpTimers"), - Description("Generates a log file of all currently executing timers. Used for tracing timer leaks.")] - public static void DumpTimers_OnCommand(CommandEventArgs e) - { - try - { - using var sw = new StreamWriter("timerdump.log", true); - Timer.DumpInfo(sw); - e.Mobile.SendMessage("Timers dumped to timerdump.log"); - } - catch - { - // ignored - } - } - - [Usage("CountObjects")] - [Description("Generates a log file detailing all item and mobile types in the world.")] - public static void CountObjects_OnCommand(CommandEventArgs e) - { - using (var op = new StreamWriter("objects.log")) - { - var table = new Dictionary(); - - foreach (var item in World.Items.Values) - { - var type = item.GetType(); - - table[type] = (table.TryGetValue(type, out var value) ? value : 0) + 1; - } - - var items = table.ToList(); - table.Clear(); - - foreach (var m in World.Mobiles.Values) - { - var type = m.GetType(); - - table[type] = (table.TryGetValue(type, out var value) ? value : 0) + 1; - } - - var mobiles = table.ToList(); - - items.Sort(new CountSorter()); - mobiles.Sort(new CountSorter()); - - op.WriteLine("# Object count table generated on {0}", Core.Now); - op.WriteLine(); - op.WriteLine(); - - op.WriteLine("# Items:"); - - items.ForEach( - kvp => - op.WriteLine("{0}\t{1:F2}%\t{2}", kvp.Value, 100.0 * kvp.Value / World.Items.Count, kvp.Key) - ); - - op.WriteLine(); - op.WriteLine(); - - op.WriteLine("#Mobiles:"); - - mobiles.ForEach( - kvp => - op.WriteLine("{0}\t{1:F2}%\t{2}", kvp.Value, 100.0 * kvp.Value / World.Mobiles.Count, kvp.Key) - ); - } - - e.Mobile.SendMessage("Object table has been generated. See the file : objects.log"); - } - - [Usage("TraceExpanded")] - [Description("Generates a log file describing all items using expanded memory.")] - public static void TraceExpanded_OnCommand(CommandEventArgs e) - { - var typeTable = new Dictionary(); - - foreach (var item in World.Items.Values) - { - var flags = item.GetExpandFlags(); - - if ((flags & ~(ExpandFlag.TempFlag | ExpandFlag.SaveFlag)) == 0) - { - continue; - } - - var itemType = item.GetType(); - - do - { - if (!typeTable.TryGetValue(itemType, out var countTable)) - { - typeTable[itemType] = countTable = new int[9]; - } - - if ((flags & ExpandFlag.Name) != 0) - { - ++countTable[0]; - } - - if ((flags & ExpandFlag.Items) != 0) - { - ++countTable[1]; - } - - if ((flags & ExpandFlag.Bounce) != 0) - { - ++countTable[2]; - } - - if ((flags & ExpandFlag.Holder) != 0) - { - ++countTable[3]; - } - - if ((flags & ExpandFlag.Blessed) != 0) - { - ++countTable[4]; - } - - /*if (( flags & ExpandFlag.TempFlag ) != 0) - ++countTable[5]; - - if (( flags & ExpandFlag.SaveFlag ) != 0) - ++countTable[6];*/ - - if ((flags & ExpandFlag.Weight) != 0) - { - ++countTable[7]; - } - - if ((flags & ExpandFlag.Spawner) != 0) - { - ++countTable[8]; - } - - itemType = itemType.BaseType; - } while (itemType != typeof(object)); - } - - try - { - using var op = new StreamWriter("expandedItems.log", true); - string[] names = - { - "Name", - "Items", - "Bounce", - "Holder", - "Blessed", - "TempFlag", - "SaveFlag", - "Weight", - "Spawner" - }; - - var list = typeTable.ToList(); - - list.Sort(new CountsSorter()); - - foreach (var kvp in list) - { - var countTable = kvp.Value; - - op.WriteLine("# {0}", kvp.Key.FullName); - - for (var i = 0; i < countTable.Length; ++i) - { - if (countTable[i] > 0) - { - op.WriteLine("{0}\t{1:N0}", names[i], countTable[i]); - } - } - - op.WriteLine(); - } - } - catch - { - // ignored - } - } - - [Usage("TraceInternal")] - [Description("Generates a log file describing all items in the 'internal' map.")] - public static void TraceInternal_OnCommand(CommandEventArgs e) - { - var totalCount = 0; - var table = new Dictionary(); - - foreach (var item in World.Items.Values) - { - if (item.Parent != null || item.Map != Map.Internal) - { - continue; - } - - ++totalCount; - - var type = item.GetType(); - - if (table.TryGetValue(type, out var parms)) - { - parms[0]++; - parms[1] += item.Amount; - } - else - { - table[type] = new[] { 1, item.Amount }; - } - } - - using var op = new StreamWriter("internal.log"); - op.WriteLine("# {0} items found", totalCount); - op.WriteLine("# {0} different types", table.Count); - op.WriteLine(); - op.WriteLine(); - op.WriteLine("Type\t\tCount\t\tAmount\t\tAvg. Amount"); - - foreach (var de in table) - { - var parms = de.Value; - - op.WriteLine("{0}\t\t{1}\t\t{2}\t\t{3:F2}", de.Key.Name, parms[0], parms[1], (double)parms[1] / parms[0]); - } - } - - [Usage("ProfileWorld"), - Description("Prints the amount of data serialized for every object type in your world file.")] - public static void ProfileWorld_OnCommand(CommandEventArgs e) - { - ProfileWorld("items", "worldprofile_items.log"); - ProfileWorld("mobiles", "worldprofile_mobiles.log"); - } - - public static void ProfileWorld(string type, string opFile) - { - try - { - var types = new List(); - - using (var bin = new BinaryReader( - new FileStream( - string.Format("Saves/{0}/{0}.tdb", type), - FileMode.Open, - FileAccess.Read, - FileShare.Read - ) - )) - { - var count = bin.ReadInt32(); - - for (var i = 0; i < count; ++i) - { - types.Add(AssemblyHandler.FindTypeByFullName(bin.ReadString())); - } - } - - long total = 0; - - var table = new Dictionary(); - - using (var bin = new BinaryReader( - new FileStream( - string.Format("Saves/{0}/{0}.idx", type), - FileMode.Open, - FileAccess.Read, - FileShare.Read - ) - )) - { - var count = bin.ReadInt32(); - - for (var i = 0; i < count; ++i) - { - var typeID = bin.ReadInt32(); - var serial = bin.ReadInt32(); - var pos = bin.ReadInt64(); - var length = bin.ReadInt32(); - var objType = types[typeID]; - - while (objType != null && objType != typeof(object)) - { - table[objType] = length + (table.TryGetValue(objType, out var value) ? value : 0); - objType = objType.BaseType; - total += length; - } - } - } - - var list = table.ToList(); - - list.Sort(new CountSorter()); - - using var op = new StreamWriter(opFile); - op.WriteLine("# Profile of world {0}", type); - op.WriteLine("# Generated on {0}", Core.Now); - op.WriteLine(); - op.WriteLine(); - - list.ForEach( - kvp => - op.WriteLine("{0}\t{1:F2}%\t{2}", kvp.Value, 100.0 * kvp.Value / total, kvp.Key) - ); - } - catch - { - // ignored - } - } - - private class CountSorter : IComparer> - { - public int Compare(KeyValuePair x, KeyValuePair y) - { - var aCount = x.Value; - var bCount = y.Value; - - var v = -aCount.CompareTo(bCount); - - return v != 0 ? v : string.CompareOrdinal(x.Key.FullName, y.Key.FullName); - } - } - - private class CountsSorter : IComparer> - { - public int Compare(KeyValuePair x, KeyValuePair y) - { - var aCount = 0; - foreach (var val in x.Value) - { - aCount += val; - } - - var bCount = 0; - foreach (var val in y.Value) - { - bCount += val; - } - - var v = -aCount.CompareTo(bCount); - - return v != 0 ? v : string.CompareOrdinal(x.Key.FullName, y.Key.FullName); - } - } - } -} diff --git a/Projects/UOContent/Diagnostics/GumpProfile.cs b/Projects/UOContent/Diagnostics/GumpProfile.cs deleted file mode 100644 index 5946c7e930..0000000000 --- a/Projects/UOContent/Diagnostics/GumpProfile.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Server.Diagnostics; - -public class GumpProfile : BaseProfile -{ - private static readonly Dictionary _profiles = new(); - - public GumpProfile(Type type) : base(type.FullName) - { - } - - public static IEnumerable Profiles => _profiles.Values; - - public static GumpProfile Acquire(Type type) - { - if (!Core.Profiling) - { - return null; - } - - if (!_profiles.TryGetValue(type, out var prof)) - { - _profiles.Add(type, prof = new GumpProfile(type)); - } - - return prof; - } -} diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchCommand.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchCommand.cs index c1027acda3..1a16e82727 100644 --- a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchCommand.cs +++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchCommand.cs @@ -14,8 +14,6 @@ public static void Configure() [Aliases("AdvSrch", "AS", "XmlFind")] private static void OnCommand(CommandEventArgs e) { - var from = e.Mobile; - - from.SendGump(new AdvancedSearchGump(from), true); + e.Mobile.SendGump(new AdvancedSearchGump(), true); } } diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchGump.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchGump.cs index 95589ab6ac..6b3048b89d 100644 --- a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchGump.cs +++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchGump.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Threading; using CommunityToolkit.HighPerformance; using Server.Commands; using Server.Commands.Generic; @@ -121,9 +122,9 @@ public bool AllSelected public int DisplayFrom { get; set; } public string CommandString { get; set; } - public AdvancedSearchGump(Mobile from) : base(50, 50) => Build(from); + public AdvancedSearchGump() : base(50, 50) => Build(); - private void Build(Mobile from) + private void Build() { const int height = 500; var haveResults = SearchResults?.Length > 0; @@ -264,12 +265,6 @@ private void Build(Mobile from) AddButton(5, y, 0xFAE, 0xFAF, 154); AddLabel(38, y, 0x384, "Bring"); - // AddButton(150, y - 25, 0xFA8, 0xFAA, 159); - // AddLabel(183, y - 25, 0x384, "Save to file:"); - // - // AddImageTiled(270, y - 25, 180, 19, 0xBBC); - // AddTextEntry(270, y - 25, 180, 19, 0, 300, SaveFilename); - AddButton(470, y - 25, 0xFA8, 0xFAA, 160); AddLabel(503, y - 25, 0x384, "Command:"); @@ -323,9 +318,12 @@ private void Build(Mobile from) AddButton(670, y, AllSelected ? 0xD3 : 0xD2, AllSelected ? 0xD2 : 0xD3, 1); var allDisplayedSelected = true; + for (int i = 0; i < MaxEntries; i++) { - var index = i + DisplayFrom; + var offset = SortDescending ? MaxEntries - 1 - i : i; + var index = offset + DisplayFrom; + if (index >= SearchResults.Length) { break; @@ -343,14 +341,14 @@ private void Build(Mobile from) if (entry.Entity is BaseSpawner) { - AddButton(145, 22 * i + 30, 0xFBD, 0xFBE, 2000 + i); + AddButton(145, 22 * i + 30, 0xFBD, 0xFBE, 2000 + offset); } // Goto button - AddButton(205, 22 * i + 30, 0xFAE, 0xFAF, 1000 + i); + AddButton(205, 22 * i + 30, 0xFAE, 0xFAF, 1000 + offset); // Interface button - AddButton(175, 22 * i + 30, 0xFAB, 0xFAD, 3000 + i); + AddButton(175, 22 * i + 30, 0xFAB, 0xFAD, 3000 + offset); var textHue = 0; var parentName = ""; @@ -394,7 +392,7 @@ private void Build(Mobile from) AddLabelCropped(640, 22 * i + 31, 90, 21, textHue, parentName); // display the selection button - AddButton(730, 22 * i + 32, entry.Selected ? 0xD3 : 0xD2, entry.Selected ? 0xD2 : 0xD3, 4000 + i); + AddButton(730, 22 * i + 32, entry.Selected ? 0xD3 : 0xD2, entry.Selected ? 0xD2 : 0xD3, 4000 + offset); } AddButton(730, 5, allDisplayedSelected ? 0xD3 : 0xD2, allDisplayedSelected ? 0xD2 : 0xD3, 2); // Select all displayed @@ -506,7 +504,7 @@ public override void OnResponse(NetState state, in RelayInfo info) case 3: // Search { DoSearch(from); - break; + return; // Don't resend the gump, it's sent async } case 154: // Bring { @@ -622,14 +620,11 @@ public override void OnResponse(NetState state, in RelayInfo info) SortDescending = !SortDescending; var radioSwitch = info.Switches.Length > 0 ? info.Switches[0] : -1; - if (radioSwitch is < 0 or > 4) - { - Array.Reverse(SearchResults); - } - else + if (radioSwitch is >= 0 and <= 4) { Sort(from); } + break; } case >= 1000 and <= 1999: // Goto @@ -780,32 +775,42 @@ private void DoSearch(Mobile from) } } - // Block until everything is processed - for (var i = 0; i < _threadWorkers.Length; i++) + ThreadPool.QueueUserWorkItem(state => { - _threadWorkers[i].Sleep(); - } + // Block until everything is processed + for (var i = 0; i < _threadWorkers.Length; i++) + { + _threadWorkers[i].Sleep(); + } - var ignoredEntities = new HashSet(ignoreQueue); + var ignoredEntities = new HashSet(ignoreQueue); - // Force the GC to collect the ignored entities - ignoreQueue.Clear(); + // Force the GC to collect the ignored entities + ignoreQueue.Clear(); - var resultsList = new List(results.Count); - foreach (var result in results) - { - if (!ignoredEntities.Contains(result.Entity)) + var resultsList = new List(results.Count); + foreach (var result in results) { - resultsList.Add(result); + if (!ignoredEntities.Contains(result.Entity)) + { + resultsList.Add(result); + } } - } - SearchResults = resultsList.ToArray(); + SearchResults = resultsList.ToArray(); - // Force the GC to collect the results - resultsList.Clear(); + // Force the GC to collect the results + resultsList.Clear(); + resultsList.TrimExcess(); - AutoSave.SavesEnabled = autoSave; + // Send the gump on the main thread + Core.LoopContext.Post( + autoSaveState => + { + AutoSave.SavesEnabled = (bool)autoSaveState!; + Resend(from); + }, state); + }, autoSave); } private void SetSortSwitches(int radioSwitch) @@ -953,7 +958,7 @@ private void ExecuteCommand(Mobile from, string commandString) public void Resend(Mobile m) { Reset(); - Build(m); + Build(); m.SendGump(this); } } diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BaseBOBEntry.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BaseBOBEntry.cs index d3bbadb45d..163dc3a328 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BaseBOBEntry.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BaseBOBEntry.cs @@ -26,6 +26,10 @@ public abstract partial class BaseBOBEntry : IBOBEntry public Serial Serial { get; } + public byte SerializedThread { get; set; } + public int SerializedPosition { get; set; } + public int SerializedLength { get; set; } + public bool Deleted { get; private set; } public BaseBOBEntry() diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs b/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs index 76c791ef1a..1aca46e316 100755 --- a/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs @@ -69,14 +69,17 @@ public partial class ChampionSpawn : Item [SerializedCommandProperty(AccessLevel.GameMaster)] private ChampionSpawnType _type; + [Tidy] [SerializableField(14)] [SerializedCommandProperty(AccessLevel.GameMaster)] private List _creatures; + [Tidy] [SerializableField(15)] [SerializedCommandProperty(AccessLevel.GameMaster)] private List _redSkulls; + [Tidy] [SerializableField(16)] [SerializedCommandProperty(AccessLevel.GameMaster)] private List _whiteSkulls; @@ -524,126 +527,128 @@ public void OnSlice() Champion = null; Stop(); } - } - else - { - int kills = _kills; - - for (var i = 0; i < _creatures.Count; ++i) - { - Mobile m = _creatures[i]; - - if (m.Deleted) - { - if (m.Corpse is { Deleted: false }) - { - ((Corpse)m.Corpse).BeginDecay(TimeSpan.FromMinutes(1)); - } - RemoveFromCreaturesAt(i); - --i; - ++_kills; - - Mobile killer = m.FindMostRecentDamager(false); + return; + } - RegisterDamageTo(m); + int kills = _kills; - if (killer is BaseCreature creature) - { - killer = creature.GetMaster(); - } + for (var i = 0; i < _creatures.Count; ++i) + { + Mobile m = _creatures[i]; - if (killer is not PlayerMobile pm) - { - continue; - } + if (!m.Deleted) + { + continue; + } - int mobSubLevel = GetSubLevelfor(m) + 1; + if (m.Corpse is { Deleted: false }) + { + ((Corpse)m.Corpse).BeginDecay(TimeSpan.FromMinutes(1)); + } - if (mobSubLevel >= 0) - { - bool gainedPath = false; + RemoveFromCreaturesAt(i); + --i; + ++_kills; - int pointsToGain = mobSubLevel * 40; + Mobile killer = m.FindMostRecentDamager(false); - if (VirtueSystem.Award(pm, VirtueName.Valor, pointsToGain, ref gainedPath)) - { - if (gainedPath) - { - pm.SendLocalizedMessage(1054032); // You have gained a path in Valor! - } - else - { - pm.SendLocalizedMessage(1054030); // You have gained in Valor! - } + RegisterDamageTo(m); - // No delay on Valor gains - } + if (killer is BaseCreature creature) + { + killer = creature.GetMaster(); + } - pm.ChampionTitles.Award(_type, mobSubLevel); - } + if (killer is not PlayerMobile pm) + { + continue; + } - if (!Core.ML) - { - continue; - } + int mobSubLevel = GetSubLevelfor(m) + 1; - var isFel = Map == Map.Felucca; - double mapChance; - if (isFel) - { - mapChance = 0.001; - } - else if (Map == Map.Ilshenar || Map == Map.Tokuno) - { - mapChance = 0.0015; - } - else // Nothing drops in Trammel, Malas or TerMur - { - continue; - } + if (mobSubLevel >= 0) + { + bool gainedPath = false; - if (Utility.RandomDouble() >= mapChance) - { - continue; - } + int pointsToGain = mobSubLevel * 40; - if (!isFel || Utility.RandomBool()) + if (VirtueSystem.Award(pm, VirtueName.Valor, pointsToGain, ref gainedPath)) + { + if (gainedPath) { - GiveScrollTo(pm, CreateRandomSoT(isFel), isFel); + pm.SendLocalizedMessage(1054032); // You have gained a path in Valor! } else { - GiveScrollTo(pm, CreateRandomPS()); + pm.SendLocalizedMessage(1054030); // You have gained in Valor! } + + // No delay on Valor gains } + + pm.ChampionTitles.Award(_type, mobSubLevel); } - // Only really needed once. - if (_kills > kills) + if (!Core.ML) { - InvalidateProperties(); + continue; } - double n = _kills / (double)MaxKills; - int p = (int)(n * 100); - - if (p >= 99) + var isFel = Map == Map.Felucca; + double mapChance; + if (isFel) { - AdvanceLevel(); + mapChance = 0.001; } - else if (p > 0) + else if (Map == Map.Ilshenar || Map == Map.Tokuno) { - SetWhiteSkullCount(p / 20); + mapChance = 0.0015; + } + else // Nothing drops in Trammel, Malas or TerMur + { + continue; + } + + if (Utility.RandomDouble() >= mapChance) + { + continue; } - if (Core.Now >= ExpireTime) + if (!isFel || Utility.RandomBool()) + { + GiveScrollTo(pm, CreateRandomSoT(isFel), isFel); + } + else { - Expire(); + GiveScrollTo(pm, CreateRandomPS()); } + } + + // Only really needed once. + if (_kills > kills) + { + InvalidateProperties(); + } + + double n = _kills / (double)MaxKills; + int p = (int)(n * 100); + + if (p >= 99) + { + AdvanceLevel(); + } + else if (p > 0) + { + SetWhiteSkullCount(p / 20); + } - Respawn(); + if (Core.Now >= ExpireTime) + { + Expire(); } + + Respawn(); } public void AdvanceLevel() @@ -1361,67 +1366,72 @@ public override void AlterLightLevel(Mobile m, ref int global, ref int personal) public override void OnEnter(Mobile m) { - if (m.Player && m.AccessLevel == AccessLevel.Player && !Spawn.Active) + if (!m.Player || m.AccessLevel != AccessLevel.Player || Spawn.Active) { - Region parent = Parent ?? this; + return; + } - if (Spawn.ReadyToActivate) + Region parent = Parent ?? this; + + if (Spawn.ReadyToActivate) + { + Spawn.Start(); + return; + } + + if (!Spawn.ProximitySpawn || Spawn.ActivatedByProximity || Core.Now < Spawn.NextProximityTime) + { + return; + } + + List players = parent.GetPlayers(); + List addresses = new List(); + for (var i = 0; i < players.Count; i++) + { + if (players[i].AccessLevel == AccessLevel.Player && players[i].NetState != null && + !addresses.Contains(players[i].NetState.Address) && !((PlayerMobile)players[i]).Young) { - Spawn.Start(); + addresses.Add(players[i].NetState.Address); } - else if (Spawn.ProximitySpawn && !Spawn.ActivatedByProximity && Core.Now >= Spawn.NextProximityTime) - { - List players = parent.GetPlayers(); - List addresses = new List(); - for (var i = 0; i < players.Count; i++) - { - if (players[i].AccessLevel == AccessLevel.Player && players[i].NetState != null && - !addresses.Contains(players[i].NetState.Address) && !((PlayerMobile)players[i]).Young) - { - addresses.Add(players[i].NetState.Address); - } - } - - if (addresses.Count >= 15) - { - foreach (Mobile player in players) - { - player.SendMessage(0x20, Spawn.BroadcastMessage); - } + } - Spawn.ActivatedByProximity = true; - Spawn.BeginRestart(TimeSpan.FromMinutes(5.0)); - } + if (addresses.Count >= 15) + { + foreach (Mobile player in players) + { + player.SendMessage(0x20, Spawn.BroadcastMessage); } + + Spawn.ActivatedByProximity = true; + Spawn.BeginRestart(TimeSpan.FromMinutes(5.0)); } } public override bool OnMoveInto(Mobile m, Direction d, Point3D newLocation, Point3D oldLocation) { - if (base.OnMoveInto(m, d, newLocation, oldLocation)) + if (!base.OnMoveInto(m, d, newLocation, oldLocation)) { - if (m.Player) - { - if (((PlayerMobile)m).Young) - { - m.SendMessage("You decide against going here because of the danger."); - } - else if (!m.Alive) - { - m.SendMessage("A magical force prevents ghosts from entering this region."); - } - else - { - return true; - } - } - else - { - return true; - } + return false; + } + + if (!m.Player) + { + return true; } - return false; + if (((PlayerMobile)m).Young) + { + m.SendMessage("You decide against going here because of the danger."); + return false; + } + + if (!m.Alive) + { + m.SendMessage("A magical force prevents ghosts from entering this region."); + return false; + } + + return true; } public override bool OnBeforeDeath(Mobile m) @@ -1454,22 +1464,24 @@ public EjectTimer(Mobile from, ChampionSpawnRegion region) : base(TimeSpan.FromM protected override void OnTick() { //See if they are dead, or logged out! - if (m_Region.Spawn != null && m_Region.CanSpawn() && !m_From.Alive) + if (m_Region.Spawn == null || !m_Region.CanSpawn() || m_From.Alive) { - if (m_From.NetState != null) - { - if (m_From.Region.IsPartOf(m_Region)) - { - m_From.MoveToWorld(m_Region.Spawn.EjectLocation, m_Region.Spawn.EjectMap); - m_From.SendMessage("A magical force forces you out of the area."); - } - } - else if (Find(m_From.LogoutLocation, m_From.LogoutMap).IsPartOf(m_Region)) + return; + } + + if (m_From.NetState != null) + { + if (m_From.Region.IsPartOf(m_Region)) { - m_From.LogoutLocation = m_Region.Spawn.EjectLocation; - m_From.LogoutMap = m_Region.Spawn.EjectMap; + m_From.MoveToWorld(m_Region.Spawn.EjectLocation, m_Region.Spawn.EjectMap); + m_From.SendMessage("A magical force forces you out of the area."); } } + else if (Find(m_From.LogoutLocation, m_From.LogoutMap).IsPartOf(m_Region)) + { + m_From.LogoutLocation = m_Region.Spawn.EjectLocation; + m_From.LogoutMap = m_Region.Spawn.EjectMap; + } } } } diff --git a/Projects/UOContent/Engines/Character Creation/CharacterCreatedEvent.cs b/Projects/UOContent/Engines/Character Creation/CharacterCreatedEvent.cs new file mode 100644 index 0000000000..4fddbf9b71 --- /dev/null +++ b/Projects/UOContent/Engines/Character Creation/CharacterCreatedEvent.cs @@ -0,0 +1,46 @@ +using Server.Accounting; +using Server.Network; + +namespace Server.Engines.CharacterCreation; + +public class CharacterCreatedEventArgs( + NetState state, IAccount a, string name, bool female, + int hue, byte[] stats, CityInfo city, (SkillName, byte)[] skills, + int shirtHue, int pantsHue, int hairId, int hairHue, + int beardId, int beardHue, int profession, Race race +) +{ + public NetState State { get; } = state; + + public IAccount Account { get; } = a; + + public Mobile Mobile { get; set; } + + public string Name { get; } = name; + + public bool Female { get; } = female; + + public int Hue { get; } = hue; + + public byte[] Stats { get; } = stats; + + public CityInfo City { get; } = city; + + public (SkillName, byte)[] Skills { get; } = skills; + + public int ShirtHue { get; } = shirtHue; + + public int PantsHue { get; } = pantsHue; + + public int HairID { get; } = hairId; + + public int HairHue { get; } = hairHue; + + public int BeardID { get; } = beardId; + + public int BeardHue { get; } = beardHue; + + public int Profession { get; set; } = profession; + + public Race Race { get; } = race; +} diff --git a/Projects/UOContent/Misc/CharacterCreation.cs b/Projects/UOContent/Engines/Character Creation/CharacterCreation.cs similarity index 93% rename from Projects/UOContent/Misc/CharacterCreation.cs rename to Projects/UOContent/Engines/Character Creation/CharacterCreation.cs index b171431e6b..b743b9da75 100644 --- a/Projects/UOContent/Misc/CharacterCreation.cs +++ b/Projects/UOContent/Engines/Character Creation/CharacterCreation.cs @@ -1,16 +1,18 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using ModernUO.CodeGeneratedEvents; using Server.Accounting; using Server.Items; using Server.Logging; using Server.Maps; +using Server.Misc; using Server.Mobiles; using Server.Network; -namespace Server.Misc; +namespace Server.Engines.CharacterCreation; -public static class CharacterCreation +public static partial class CharacterCreation { private static readonly ILogger logger = LogFactory.GetLogger(typeof(CharacterCreation)); @@ -78,45 +80,45 @@ public static class CharacterCreation public static readonly CityInfo[] OldHavenStartingCities = [ - new CityInfo("Haven", "The Bountiful Harvest Inn", 3677, 2625, 0, Map.Trammel), - new CityInfo("Britain", "Sweet Dreams Inn", 1075074, 1496, 1628, 10, Map.Trammel), - new CityInfo("Magincia", "The Great Horns Tavern", 1075077, 3734, 2222, 20, Map.Trammel), + new("Haven", "The Bountiful Harvest Inn", 3677, 2625, 0, Map.Trammel), + new("Britain", "Sweet Dreams Inn", 1075074, 1496, 1628, 10, Map.Trammel), + new("Magincia", "The Great Horns Tavern", 1075077, 3734, 2222, 20, Map.Trammel), ]; public static readonly CityInfo[] FeluccaStartingCities = [ - new CityInfo("Yew", "The Empath Abbey", 1075072, 633, 858, 0, Map.Felucca), - new CityInfo("Minoc", "The Barnacle", 1075073, 2476, 413, 15, Map.Felucca), - new CityInfo("Britain", "Sweet Dreams Inn", 1075074, 1496, 1628, 10, Map.Felucca), - new CityInfo("Moonglow", "The Scholars Inn", 1075075, 4408, 1168, 0, Map.Felucca), - new CityInfo("Trinsic", "The Traveler's Inn", 1075076, 1845, 2745, 0, Map.Felucca), - new CityInfo("Magincia", "The Great Horns Tavern", 1075077, 3734, 2222, 20, Map.Felucca), - new CityInfo("Jhelom", "The Mercenary Inn", 1075078, 1374, 3826, 0, Map.Felucca), - new CityInfo("Skara Brae", "The Falconer's Inn", 1075079, 618, 2234, 0, Map.Felucca), - new CityInfo("Vesper", "The Ironwood Inn", 1075080, 2771, 976, 0, Map.Felucca) + new("Yew", "The Empath Abbey", 1075072, 633, 858, 0, Map.Felucca), + new("Minoc", "The Barnacle", 1075073, 2476, 413, 15, Map.Felucca), + new("Britain", "Sweet Dreams Inn", 1075074, 1496, 1628, 10, Map.Felucca), + new("Moonglow", "The Scholars Inn", 1075075, 4408, 1168, 0, Map.Felucca), + new("Trinsic", "The Traveler's Inn", 1075076, 1845, 2745, 0, Map.Felucca), + new("Magincia", "The Great Horns Tavern", 1075077, 3734, 2222, 20, Map.Felucca), + new("Jhelom", "The Mercenary Inn", 1075078, 1374, 3826, 0, Map.Felucca), + new("Skara Brae", "The Falconer's Inn", 1075079, 618, 2234, 0, Map.Felucca), + new("Vesper", "The Ironwood Inn", 1075080, 2771, 976, 0, Map.Felucca) ]; public static readonly CityInfo[] TrammelStartingCities = [ - new CityInfo("Yew", "The Empath Abbey", 1075072, 633, 858, 0, Map.Trammel), - new CityInfo("Minoc", "The Barnacle", 1075073, 2476, 413, 15, Map.Trammel), - new CityInfo("Moonglow", "The Scholars Inn", 1075075, 4408, 1168, 0, Map.Trammel), - new CityInfo("Trinsic", "The Traveler's Inn", 1075076, 1845, 2745, 0, Map.Trammel), - new CityInfo("Jhelom", "The Mercenary Inn", 1075078, 1374, 3826, 0, Map.Trammel), - new CityInfo("Skara Brae", "The Falconer's Inn", 1075079, 618, 2234, 0, Map.Trammel), - new CityInfo("Vesper", "The Ironwood Inn", 1075080, 2771, 976, 0, Map.Trammel), + new("Yew", "The Empath Abbey", 1075072, 633, 858, 0, Map.Trammel), + new("Minoc", "The Barnacle", 1075073, 2476, 413, 15, Map.Trammel), + new("Moonglow", "The Scholars Inn", 1075075, 4408, 1168, 0, Map.Trammel), + new("Trinsic", "The Traveler's Inn", 1075076, 1845, 2745, 0, Map.Trammel), + new("Jhelom", "The Mercenary Inn", 1075078, 1374, 3826, 0, Map.Trammel), + new("Skara Brae", "The Falconer's Inn", 1075079, 618, 2234, 0, Map.Trammel), + new("Vesper", "The Ironwood Inn", 1075080, 2771, 976, 0, Map.Trammel), ]; public static readonly CityInfo[] NewHavenStartingCities = [ - new CityInfo("New Haven", "The Bountiful Harvest Inn", 1150168, 3503, 2574, 14, Map.Trammel), - new CityInfo("Britain", "The Wayfarer's Inn", 1075074, 1602, 1591, 20, Map.Trammel) + new("New Haven", "The Bountiful Harvest Inn", 1150168, 3503, 2574, 14, Map.Trammel), + new("Britain", "The Wayfarer's Inn", 1075074, 1602, 1591, 20, Map.Trammel) // Magincia removed because it burned down. ]; public static readonly CityInfo[] StartingCitiesSA = [ - new CityInfo("Royal City", "Royal City Inn", 1150169, 738, 3486, -19, Map.TerMur) + new("Royal City", "Royal City Inn", 1150169, 738, 3486, -19, Map.TerMur) ]; private static CityInfo[] _availableStartingCities; @@ -155,11 +157,8 @@ private static CityInfo[] ConstructAvailableStartingCities() return []; } - public static void Initialize() - { - // Register our event handler - EventSink.CharacterCreated += EventSink_CharacterCreated; - } + [GeneratedEvent(nameof(CharacterCreatedEvent))] + public static partial void CharacterCreatedEvent(CharacterCreatedEventArgs e); private static void AddBackpack(this Mobile m) { @@ -197,7 +196,8 @@ private static Mobile CreateMobile(Account a) return null; } - private static void EventSink_CharacterCreated(CharacterCreatedEventArgs args) + [OnEvent(nameof(CharacterCreatedEvent))] + private static void OnCharacterCreated(CharacterCreatedEventArgs args) { if (!ProfessionInfo.GetProfession(args.Profession, out var profession)) { @@ -396,24 +396,13 @@ private static CityInfo GetStartLocation(CharacterCreatedEventArgs args) return args.City; } - private static void SetStats(Mobile m, NetState state, StatNameValue[] stats) + private static void SetStats(Mobile m, NetState state, byte[] stats) { var maxStats = state.NewCharacterCreation ? 90 : 80; - var str = 0; - var dex = 0; - var intel = 0; - - for (var i = 0; i < stats.Length; i++) - { - var (statType, value) = stats[i]; - switch (statType) - { - case StatType.Str: str = value; break; - case StatType.Dex: dex = value; break; - case StatType.Int: intel = value; break; - } - } + var str = stats[0]; + var dex = stats[1]; + var intel = stats[2]; if (str is < 10 or > 60 || dex is < 10 or > 60 || intel is < 10 or > 60 || str + dex + intel != maxStats) { @@ -437,7 +426,7 @@ private static void SetName(Mobile m, string name) m.Name = name; } - private static bool ValidateSkills(int raceFlag, SkillNameValue[] skills) + private static bool ValidateSkills(int raceFlag, (SkillName, byte)[] skills) { var total = 0; @@ -445,7 +434,7 @@ private static bool ValidateSkills(int raceFlag, SkillNameValue[] skills) { var (name, value) = skills[i]; - if (value is < 0 or > 50 || !_allowedStartingSkills.Contains(name)) + if (value > 50 || !_allowedStartingSkills.Contains(name)) { return false; } @@ -473,6 +462,7 @@ private static bool ValidateSkills(int raceFlag, SkillNameValue[] skills) for (var j = i + 1; j < skills.Length; ++j) { var (nameCheck, valueCheck) = skills[j]; + if (valueCheck > 0 && nameCheck == name) { return false; @@ -483,7 +473,7 @@ private static bool ValidateSkills(int raceFlag, SkillNameValue[] skills) return total is 100 or 120; } - private static void SetSkills(Mobile m, SkillNameValue[] skills) + private static void SetSkills(Mobile m, (SkillName, byte)[] skills) { if (!ValidateSkills(m.Race.RaceFlag, skills)) { diff --git a/Projects/UOContent/Engines/ConPVP/DuelContext.cs b/Projects/UOContent/Engines/ConPVP/DuelContext.cs index c44bb85c23..6004b6ba34 100644 --- a/Projects/UOContent/Engines/ConPVP/DuelContext.cs +++ b/Projects/UOContent/Engines/ConPVP/DuelContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Engines.PartySystem; using Server.Factions; using Server.Gumps; @@ -596,6 +597,9 @@ public void OnLocationChanged(Mobile mob) } } + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + public static void OnPlayerDeath(PlayerMobile m) => m.DuelContext?.OnDeath(m, m.Corpse); + public void OnDeath(Mobile mob, Container corpse) { if (!Registered || !Started) @@ -1843,7 +1847,7 @@ public static void Debuff(Mobile mob) } TransformationSpellHelper.RemoveContext(mob, true); - AnimalForm.RemoveContext(mob, true); + AnimalForm.RemoveContext(mob); DisguisePersistence.StopTimer(mob); if (!mob.CanBeginAction()) diff --git a/Projects/UOContent/Engines/Ethics/Core/Ethic.cs b/Projects/UOContent/Engines/Ethics/Core/Ethic.cs index 22bb85c799..7c38c8c362 100644 --- a/Projects/UOContent/Engines/Ethics/Core/Ethic.cs +++ b/Projects/UOContent/Engines/Ethics/Core/Ethic.cs @@ -10,37 +10,23 @@ namespace Server.Ethics; [SerializationGenerator(1)] public abstract partial class Ethic : EthicsEntity { - public static Ethic Hero { get; private set; } - public static Ethic Evil { get; private set; } + public static Ethic Hero => Ethics[0]; + public static Ethic Evil => Ethics[1]; - public static readonly Ethic[] Ethics = - { - Hero, - Evil - }; + private static Ethic[] Ethics => [null, null]; public static bool RegisterEthic(Ethic ethic) { - if (ethic is HeroEthic) + if (ethic is HeroEthic && Hero == null) { - if (Hero == null) - { - Hero = ethic; - return true; - } - - return false; + Ethics[0] = ethic; + return true; } - if (ethic is EvilEthic) + if (ethic is EvilEthic && Evil == null) { - if (Evil == null) - { - Evil = ethic; - return true; - } - - return false; + Ethics[1] = ethic; + return true; } return false; @@ -58,6 +44,11 @@ public static Ethic Find(Item item) { if ((item.SavedFlags & 0x100) != 0) { + if (Hero == null) + { + return null; + } + if (item.Hue == Hero.Definition.PrimaryHue) { return Hero; @@ -68,6 +59,11 @@ public static Ethic Find(Item item) if ((item.SavedFlags & 0x200) != 0) { + if (Evil == null) + { + return null; + } + if (item.Hue == Evil.Definition.PrimaryHue) { return Evil; @@ -172,7 +168,7 @@ public static void EventSink_Speech(SpeechEventArgs e) { var ethic = Ethics[i]; - if (!ethic.IsEligible(e.Mobile)) + if (ethic?.IsEligible(e.Mobile) != true) { continue; } @@ -240,11 +236,7 @@ public static void EventSink_Speech(SpeechEventArgs e) } } - public static Ethic Find(Mobile mob) => Find(mob, false, false); - - public static Ethic Find(Mobile mob, bool inherit) => Find(mob, inherit, false); - - public static Ethic Find(Mobile mob, bool inherit, bool allegiance) + public static Ethic Find(Mobile mob, bool inherit = false, bool allegiance = false) { var pl = Player.Find(mob); @@ -255,14 +247,10 @@ public static Ethic Find(Mobile mob, bool inherit, bool allegiance) if (inherit && mob is BaseCreature bc) { - if (bc.Controlled) - { - return Find(bc.ControlMaster, false); - } - - if (bc.Summoned) + var master = bc.GetMaster(); + if (master != null) { - return Find(bc.SummonMaster, false); + return Find(master); } if (allegiance) diff --git a/Projects/UOContent/Engines/Ethics/Core/EthicsEntity.cs b/Projects/UOContent/Engines/Ethics/Core/EthicsEntity.cs index 5ce677cb81..aaf5af3be4 100644 --- a/Projects/UOContent/Engines/Ethics/Core/EthicsEntity.cs +++ b/Projects/UOContent/Engines/Ethics/Core/EthicsEntity.cs @@ -16,6 +16,10 @@ public EthicsEntity() public Serial Serial { get; } + public byte SerializedThread { get; set; } + public int SerializedPosition { get; set; } + public int SerializedLength { get; set; } + public bool Deleted { get; private set; } public void Delete() diff --git a/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholyFamiliar.cs b/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholyFamiliar.cs index 84632e394f..6b46807d65 100644 --- a/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholyFamiliar.cs +++ b/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholyFamiliar.cs @@ -54,15 +54,19 @@ public UnholyFamiliar() : base(AIType.AI_Melee) public override string ApplyNameSuffix(string suffix) { - if (suffix.Length == 0) + var ethic = Ethic.Evil; + if (ethic == null) { - suffix = Ethic.Evil.Definition.Adjunct.String; + return base.ApplyNameSuffix(""); } - else + + var adjunct = ethic.Definition.Adjunct; + + if (suffix.Length == 0) { - suffix = $"{suffix} {Ethic.Evil.Definition.Adjunct.String}"; + return base.ApplyNameSuffix(adjunct); } - return base.ApplyNameSuffix(suffix); + return base.ApplyNameSuffix($"{suffix} {adjunct}"); } } diff --git a/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholySteed.cs b/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholySteed.cs index 9c8360e666..7ac77c40cc 100644 --- a/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholySteed.cs +++ b/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholySteed.cs @@ -53,17 +53,25 @@ public UnholySteed() : base(0x74, 0x3EA7, AIType.AI_Melee, FightMode.Aggressor) public override string ApplyNameSuffix(string suffix) { + var ethic = Ethic.Evil; + if (ethic == null) + { + return base.ApplyNameSuffix(""); + } + + var adjunct = ethic.Definition.Adjunct; + if (suffix.Length == 0) { - return base.ApplyNameSuffix(Ethic.Evil.Definition.Adjunct.String); + return base.ApplyNameSuffix(adjunct); } - return base.ApplyNameSuffix($"{suffix} {Ethic.Evil.Definition.Adjunct.String}"); + return base.ApplyNameSuffix($"{suffix} {adjunct}"); } public override void OnDoubleClick(Mobile from) { - if (Ethic.Find(from) != Ethic.Evil) + if (Ethic.Evil == null || Ethic.Find(from) != Ethic.Evil) { from.SendMessage("You may not ride this steed."); } diff --git a/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolyFamiliar.cs b/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolyFamiliar.cs index 37dc99fba7..9acdc8b1ed 100644 --- a/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolyFamiliar.cs +++ b/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolyFamiliar.cs @@ -55,15 +55,19 @@ public HolyFamiliar() : base(AIType.AI_Melee) public override string ApplyNameSuffix(string suffix) { - if (suffix.Length == 0) + var ethic = Ethic.Hero; + if (ethic == null) { - suffix = Ethic.Hero.Definition.Adjunct.String; + return base.ApplyNameSuffix(""); } - else + + var adjunct = ethic.Definition.Adjunct; + + if (suffix.Length == 0) { - suffix = $"{suffix} {Ethic.Hero.Definition.Adjunct.String}"; + return base.ApplyNameSuffix(adjunct); } - return base.ApplyNameSuffix(suffix); + return base.ApplyNameSuffix($"{suffix} {adjunct}"); } } diff --git a/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolySteed.cs b/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolySteed.cs index 7bb0eab5c5..edb076b799 100644 --- a/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolySteed.cs +++ b/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolySteed.cs @@ -53,17 +53,25 @@ public HolySteed() : base(0x75, 0x3EA8, AIType.AI_Melee, FightMode.Aggressor) public override string ApplyNameSuffix(string suffix) { + var ethic = Ethic.Hero; + if (ethic == null) + { + return base.ApplyNameSuffix(""); + } + + var adjunct = ethic.Definition.Adjunct; + if (suffix.Length == 0) { - return base.ApplyNameSuffix(Ethic.Hero.Definition.Adjunct.String); + return base.ApplyNameSuffix(adjunct); } - return base.ApplyNameSuffix($"{suffix} {Ethic.Hero.Definition.Adjunct.String}"); + return base.ApplyNameSuffix($"{suffix} {adjunct}"); } public override void OnDoubleClick(Mobile from) { - if (Ethic.Find(from) != Ethic.Hero) + if (Ethic.Hero == null || Ethic.Find(from) != Ethic.Hero) { from.SendMessage("You may not ride this steed."); } diff --git a/Projects/UOContent/Engines/ML Quests/MLQuestSystem.cs b/Projects/UOContent/Engines/ML Quests/MLQuestSystem.cs index b028ddc856..d583c6be78 100644 --- a/Projects/UOContent/Engines/ML Quests/MLQuestSystem.cs +++ b/Projects/UOContent/Engines/ML Quests/MLQuestSystem.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using ModernUO.CodeGeneratedEvents; using Server.Commands; using Server.Commands.Generic; using Server.Engines.MLQuests.Gumps; @@ -611,6 +612,7 @@ public static MLQuestContext GetOrCreateContext(PlayerMobile pm) return context; } + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] public static void HandleDeath(PlayerMobile pm) { var context = GetContext(pm); diff --git a/Projects/UOContent/Engines/Party/Party.cs b/Projects/UOContent/Engines/Party/Party.cs index 23988158a1..e474885c8c 100644 --- a/Projects/UOContent/Engines/Party/Party.cs +++ b/Projects/UOContent/Engines/Party/Party.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Factions; +using Server.Mobiles; using Server.Network; using Server.Targeting; @@ -99,7 +101,6 @@ public void OnStatsQuery(Mobile beholder, Mobile beheld) public static void Configure() { EventSink.Logout += EventSink_Logout; - EventSink.PlayerDeath += EventSink_PlayerDeath; CommandSystem.Register("ListenToParty", AccessLevel.GameMaster, ListenToParty_OnCommand); } @@ -135,7 +136,8 @@ public static void ListenToParty_OnTarget(Mobile from, object obj) } } - public static void EventSink_PlayerDeath(Mobile from) + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + public static void OnPlayerDeathEvent(Mobile from) { var p = Get(from); diff --git a/Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs b/Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs index 7704444396..31b4a6bc30 100644 --- a/Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs +++ b/Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Gumps; using Server.Misc; using Server.Mobiles; @@ -26,16 +27,16 @@ private ReportMurdererGump(List killers, int idx = 0) : base(0, 0) public static void Initialize() { _recentlyReportedDelay = ServerConfiguration.GetOrUpdateSetting("murderSystem.recentlyReportedDelay", TimeSpan.FromMinutes(10)); - EventSink.PlayerDeath += OnPlayerDeath; } - public static void OnPlayerDeath(Mobile m) + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + public static void OnPlayerDeathEvent(PlayerMobile m) { List killers = null; HashSet toGive = null; // Guards won't take reports of the death of a thief! - bool notInThievesGuild = m is not PlayerMobile { NpcGuild: NpcGuild.ThievesGuild }; + bool notInThievesGuild = m.NpcGuild != NpcGuild.ThievesGuild; foreach (var ai in m.Aggressors) { diff --git a/Projects/UOContent/Engines/Virtues/Justice.cs b/Projects/UOContent/Engines/Virtues/Justice.cs index 01afa4de60..abbb8da1e9 100644 --- a/Projects/UOContent/Engines/Virtues/Justice.cs +++ b/Projects/UOContent/Engines/Virtues/Justice.cs @@ -227,7 +227,7 @@ public static void OnVirtueAccepted(PlayerMobile protector, PlayerMobile protect } else { - AddProtection(protectee, protector); + AddProtection(protector, protectee); var args = $"{protector.Name}\t{protectee.Name}"; @@ -267,67 +267,75 @@ public static void CheckAtrophy(PlayerMobile pm) } } -public class AcceptProtectorGump : Gump +public class AcceptProtectorGump : StaticGump { - private readonly PlayerMobile _protectee; private readonly PlayerMobile _protector; + private readonly PlayerMobile _protectee; public AcceptProtectorGump(PlayerMobile protector, PlayerMobile protectee) : base(150, 50) { _protector = protector; _protectee = protectee; + } - Closable = false; + protected override void BuildLayout(ref StaticGumpBuilder builder) + { + builder.SetNoClose(); - AddPage(0); + builder.AddPage(); - AddBackground(0, 0, 396, 218, 3600); + builder.AddBackground(0, 0, 396, 218, 3600); - AddImageTiled(15, 15, 365, 190, 2624); - AddAlphaRegion(15, 15, 365, 190); + builder.AddImageTiled(15, 15, 365, 190, 2624); + builder.AddAlphaRegion(15, 15, 365, 190); // Another player is offering you their protection: - AddHtmlLocalized(30, 20, 360, 25, 1049365, 0x7FFF); - AddLabel(90, 55, 1153, protector.Name); + builder.AddHtmlLocalized(30, 20, 360, 25, 1049365, 0x7FFF); + builder.AddLabelPlaceholder(90, 55, 1153, "protector"); - AddImage(50, 45, 9005); - AddImageTiled(80, 80, 200, 1, 9107); - AddImageTiled(95, 82, 200, 1, 9157); + builder.AddImage(50, 45, 9005); + builder.AddImageTiled(80, 80, 200, 1, 9107); + builder.AddImageTiled(95, 82, 200, 1, 9157); - AddRadio(30, 110, 9727, 9730, true, 1); - AddHtmlLocalized(65, 115, 300, 25, 1049444, 0x7FFF); // Yes, I would like their protection. + builder.AddRadio(30, 110, 9727, 9730, true, 1); + builder.AddHtmlLocalized(65, 115, 300, 25, 1049444, 0x7FFF); // Yes, I would like their protection. - AddRadio(30, 145, 9727, 9730, false, 0); - AddHtmlLocalized(65, 148, 300, 25, 1049445, 0x7FFF); // No thanks, I can take care of myself. + builder.AddRadio(30, 145, 9727, 9730, false, 0); + builder.AddHtmlLocalized(65, 148, 300, 25, 1049445, 0x7FFF); // No thanks, I can take care of myself. - AddButton(160, 175, 247, 248, 2); + builder.AddButton(160, 175, 247, 248, 2); - AddImage(215, 0, 50581); + builder.AddImage(215, 0, 50581); - 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("protector", _protector.Name); } public override void OnResponse(NetState sender, in RelayInfo info) { - if (info.ButtonID == 2) + if (info.ButtonID != 2) { - var okay = info.IsSwitched(1); + return; + } - if (okay) - { - JusticeVirtue.OnVirtueAccepted(_protector, _protectee); - } - else - { - JusticeVirtue.OnVirtueRejected(_protector, _protectee); - } + if (info.IsSwitched(1)) // okay + { + JusticeVirtue.OnVirtueAccepted(_protector, _protectee); + } + else + { + JusticeVirtue.OnVirtueRejected(_protector, _protectee); } } } diff --git a/Projects/UOContent/Gumps/AdminGump.cs b/Projects/UOContent/Gumps/AdminGump.cs index 17d1f693aa..43d6c1bef5 100644 --- a/Projects/UOContent/Gumps/AdminGump.cs +++ b/Projects/UOContent/Gumps/AdminGump.cs @@ -169,7 +169,7 @@ public AdminGump( AddLabel(150, 150, LabelHue, banned.ToString()); AddLabel(20, 170, LabelHue, "Firewalled:"); - AddLabel(150, 170, LabelHue, AdminFirewall.Set.Count.ToString()); + AddLabel(150, 170, LabelHue, Firewall.FirewallSet.Count.ToString()); AddLabel(20, 190, LabelHue, "Clients:"); AddLabel(150, 190, LabelHue, NetState.Instances.Count.ToString()); @@ -1161,7 +1161,7 @@ public AdminGump( { AddFirewallHeader(); - m_List ??= AdminFirewall.Set.ToList(); + m_List ??= Firewall.FirewallSet.ToList(); AddLabelCropped(12, 120, 358, 20, LabelHue, "IP Address"); @@ -3464,7 +3464,7 @@ public override void OnResponse(NetState sender, in RelayInfo info) } else { - foreach (var check in AdminFirewall.Set) + foreach (var check in Firewall.FirewallSet) { var checkStr = check.ToString(); @@ -3968,7 +3968,14 @@ private void Shutdown(bool restart, bool save) InvokeCommand("Save"); } - Core.Kill(restart); + // Kill the server on a different thread otherwise we will dead lock + ThreadPool.QueueUserWorkItem( + _ => + { + World.WaitForWriteCompletion(); + Core.Kill(restart); + } + ); } private void InvokeCommand(string c) diff --git a/Projects/UOContent/Gumps/Base/GumpSystem.IncomingPackets.cs b/Projects/UOContent/Gumps/Base/GumpSystem.IncomingPackets.cs index e1d6d033ba..b0d2427c8a 100644 --- a/Projects/UOContent/Gumps/Base/GumpSystem.IncomingPackets.cs +++ b/Projects/UOContent/Gumps/Base/GumpSystem.IncomingPackets.cs @@ -13,7 +13,6 @@ * along with this program. If not, see . * *************************************************************************/ -using Server.Diagnostics; using Server.Engines.Virtues; using Server.Exceptions; using Server.Mobiles; @@ -157,10 +156,6 @@ public static void DisplayGumpResponse(NetState state, SpanReader reader) Remove(state, baseGump); - var prof = GumpProfile.Acquire(baseGump.GetType()); - - prof?.Start(); - var relayInfo = new RelayInfo( buttonId, switches, @@ -169,8 +164,6 @@ public static void DisplayGumpResponse(NetState state, SpanReader reader) textBlock ); baseGump.OnResponse(state, relayInfo); - - prof?.Finish(); } if (typeId == 461) diff --git a/Projects/UOContent/Gumps/RunebookGump.cs b/Projects/UOContent/Gumps/RunebookGump.cs index 58d7716358..7134e3e02e 100644 --- a/Projects/UOContent/Gumps/RunebookGump.cs +++ b/Projects/UOContent/Gumps/RunebookGump.cs @@ -133,14 +133,14 @@ private void AddIndex(ref DynamicGumpBuilder builder) for (var i = 0; i < 16; ++i) { - var entry = entries[i]; - // Use charge button builder.AddButton(130 + i / 8 * 160, 65 + i % 8 * 15, 2103, 2104, 2 + i * 6 + 0); // Description label if (i < entries.Count) { + var entry = entries[i]; + builder.AddLabelCropped( 145 + i / 8 * 160, 60 + i % 8 * 15, @@ -257,7 +257,7 @@ public override void OnResponse(NetState state, in RelayInfo info) var index = Math.DivRem(buttonID, 6, out var type); - if (index < 0 || index > _book.Entries.Count) + if (index < 0 || index >= _book.Entries.Count) { return; } diff --git a/Projects/UOContent/Holiday Stuff/Halloween/2012/Engines/PlayerZombies.cs b/Projects/UOContent/Holiday Stuff/Halloween/2012/Engines/PlayerZombies.cs index 30e41249ef..b02a4aba2f 100644 --- a/Projects/UOContent/Holiday Stuff/Halloween/2012/Engines/PlayerZombies.cs +++ b/Projects/UOContent/Holiday Stuff/Halloween/2012/Engines/PlayerZombies.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using ModernUO.Serialization; using Server.Events.Halloween; using Server.Items; @@ -12,15 +13,15 @@ public static class HalloweenHauntings private static Timer _timer; private static Timer _clearTimer; - private static int m_TotalZombieLimit; - private static int m_DeathQueueLimit; - private static int m_QueueDelaySeconds; - private static int m_QueueClearIntervalSeconds; + private const int TotalZombieLimit = 200; + private const int DeathQueueLimit = 200; + private const int QueueDelaySeconds = 120; + private const int QueueClearIntervalSeconds = 1800; private static HashSet _deathQueue; - private static readonly Rectangle2D[] m_Cemetaries = - { + private static readonly Rectangle2D[] _cemetaries = + [ new(1272, 3712, 30, 20), // Jhelom new(1337, 1444, 48, 52), // Britain new(2424, 1098, 20, 28), // Trinsic @@ -38,39 +39,31 @@ public static class HalloweenHauntings new(712, 1104, 22, 30), // Yew new(5824, 1464, 6, 22), // Fire Dungeon new(5224, 3655, 5, 14) // T2A - }; + ]; internal static Dictionary _reAnimated; - public static void Initialize() + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + public static void OnPlayerDeathEvent(PlayerMobile pm) { - m_TotalZombieLimit = 200; - m_DeathQueueLimit = 200; - m_QueueDelaySeconds = 120; - m_QueueClearIntervalSeconds = 1800; - - var today = Core.Now; - var tick = TimeSpan.FromSeconds(m_QueueDelaySeconds); - var clear = TimeSpan.FromSeconds(m_QueueClearIntervalSeconds); - - _reAnimated = new Dictionary(); - _deathQueue = new HashSet(); + var now = Core.Now; - if (today >= HolidaySettings.StartHalloween && today <= HolidaySettings.FinishHalloween) + if (now < HolidaySettings.StartHalloween || now > HolidaySettings.FinishHalloween) { - _timer = Timer.DelayCall(tick, 0, Timer_Callback); - _clearTimer = Timer.DelayCall(clear, 0, Clear_Callback); - - EventSink.PlayerDeath += EventSink_PlayerDeath; + return; } - } - public static void EventSink_PlayerDeath(Mobile m) - { - if (m is PlayerMobile { Deleted: false } pm && - _timer.Running && !_deathQueue.Contains(pm) && _deathQueue.Count < m_DeathQueueLimit) + _timer ??= Timer.DelayCall(TimeSpan.FromSeconds(QueueDelaySeconds), 0, Timer_Callback); + _clearTimer ??= Timer.DelayCall(TimeSpan.FromSeconds(QueueClearIntervalSeconds), 0, Clear_Callback); + + if (_timer.Running) { - _deathQueue.Add(pm); + _deathQueue ??= []; + + if (_deathQueue.Count < DeathQueueLimit) + { + _deathQueue.Add(pm); + } } } @@ -85,13 +78,12 @@ private static void Clear_Callback() return; } - _reAnimated.Clear(); - _deathQueue.Clear(); + _reAnimated?.Clear(); + _deathQueue?.Clear(); } private static void Timer_Callback() { - if (Core.Now > HolidaySettings.FinishHalloween) { _timer.Stop(); @@ -99,35 +91,40 @@ private static void Timer_Callback() return; } - PlayerMobile player = null; + if (_deathQueue == null) + { + return; + } + PlayerMobile player = null; foreach (var entry in _deathQueue) { - if (!_reAnimated.ContainsKey(entry)) + if (_reAnimated?.ContainsKey(entry) != true) { player = entry; break; } } - if (player?.Deleted != false || _reAnimated.Count >= m_TotalZombieLimit) + if (player?.Deleted != false || _reAnimated?.Count >= TotalZombieLimit) { return; } var map = Utility.RandomBool() ? Map.Trammel : Map.Felucca; - var home = Utility.RandomPointIn(m_Cemetaries.RandomElement(), map); + var home = Utility.RandomPointIn(_cemetaries.RandomElement(), map); if (map.CanSpawnMobile(home)) { var zombieskel = new ZombieSkeleton(player); + _reAnimated ??= []; _reAnimated.Add(player, zombieskel); + zombieskel.Home = home; zombieskel.RangeHome = 10; zombieskel.MoveToWorld(home, map); - _deathQueue.Remove(player); } } diff --git a/Projects/UOContent/Items/Skill Items/Thief/DisguisePersistence.cs b/Projects/UOContent/Items/Skill Items/Thief/DisguisePersistence.cs index a6cd023a11..9675ed68b4 100644 --- a/Projects/UOContent/Items/Skill Items/Thief/DisguisePersistence.cs +++ b/Projects/UOContent/Items/Skill Items/Thief/DisguisePersistence.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Mobiles; namespace Server.Items; @@ -69,6 +70,7 @@ public static void StopTimer(Mobile m) t.Stop(); } + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] public static void RemoveTimer(Mobile m) { if (Timers.Remove(m, out var t)) diff --git a/Projects/UOContent/Items/Special/8th Anniversary Items/Talismans.cs b/Projects/UOContent/Items/Special/8th Anniversary Items/Talismans.cs index 83cf1ff952..091cc242b6 100644 --- a/Projects/UOContent/Items/Special/8th Anniversary Items/Talismans.cs +++ b/Projects/UOContent/Items/Special/8th Anniversary Items/Talismans.cs @@ -36,7 +36,7 @@ public override void OnRemoved(IEntity parent) if (parent is Mobile m) { - AnimalForm.RemoveContext(m, true); + AnimalForm.RemoveContext(m); } } diff --git a/Projects/UOContent/Items/Talismans/TalismanSummons.cs b/Projects/UOContent/Items/Talismans/TalismanSummons.cs index 07b6e1acd0..8c46688bb3 100644 --- a/Projects/UOContent/Items/Talismans/TalismanSummons.cs +++ b/Projects/UOContent/Items/Talismans/TalismanSummons.cs @@ -45,7 +45,7 @@ public override void OnClick(Mobile from, IEntity target) Effects.PlaySound(from,0x201); - from.Delete(); + target.Delete(); } } } diff --git a/Projects/UOContent/Items/Weapons/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs index cbc6fabd5c..ccec00c089 100644 --- a/Projects/UOContent/Items/Weapons/BaseWeapon.cs +++ b/Projects/UOContent/Items/Weapons/BaseWeapon.cs @@ -2552,12 +2552,9 @@ public virtual double ScaleDamageAOS(Mobile attacker, double damage, bool checkS var strengthBonus = GetBonus(attacker.Str, 0.300, 100.0, 5.00); var anatomyBonus = GetBonus(attacker.Skills.Anatomy.Value, 0.500, 100.0, 5.00); var tacticsBonus = GetBonus(attacker.Skills.Tactics.Value, 0.625, 100.0, 6.25); - var lumberBonus = GetBonus(attacker.Skills.Lumberjacking.Value, 0.200, 100.0, 10.00); - - if (Type != WeaponType.Axe) - { - lumberBonus = 0.0; - } + var lumberBonus = Type == WeaponType.Axe + ? GetBonus(attacker.Skills.Lumberjacking.Value, 0.200, 100.0, 10.00) + : 0.0; /* * The following are damage modifiers whose effect shows on the status bar. @@ -2595,9 +2592,10 @@ public virtual double ScaleDamageAOS(Mobile attacker, double damage, bool checkS damageBonus = 100; } - var totalBonus = strengthBonus + anatomyBonus + tacticsBonus + lumberBonus + damageBonus + GetDamageBonus(); + var totalBonus = strengthBonus + anatomyBonus + tacticsBonus + lumberBonus + + (damageBonus + GetDamageBonus()) / 100.0; - return damage + damage * totalBonus / 100.0; + return damage + damage * totalBonus; } public virtual int ComputeDamageAOS(Mobile attacker, Mobile defender) => @@ -3111,7 +3109,7 @@ public override void GetProperties(IPropertyList list) list.Add(1111917); // Immolated } - if (Core.ML && (ranged?.Velocity ?? 0) != 0) + if (Core.ML && (prop = ranged?.Velocity ?? 0) != 0) { list.Add(1072793, prop); // Velocity ~1_val~% } diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v10.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v10.json index b04ef31d08..80650835fc 100644 --- a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v10.json +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v10.json @@ -119,6 +119,7 @@ "type": "System.Collections.Generic.List\u003CServer.Mobile\u003E", "rule": "ListMigrationRule", "ruleArguments": [ + "@Tidy", "Server.Mobile", "SerializableInterfaceMigrationRule" ] @@ -128,6 +129,7 @@ "type": "System.Collections.Generic.List\u003CServer.Item\u003E", "rule": "ListMigrationRule", "ruleArguments": [ + "@Tidy", "Server.Item", "SerializableInterfaceMigrationRule" ] @@ -137,6 +139,7 @@ "type": "System.Collections.Generic.List\u003CServer.Item\u003E", "rule": "ListMigrationRule", "ruleArguments": [ + "@Tidy", "Server.Item", "SerializableInterfaceMigrationRule" ] diff --git a/Projects/UOContent/Misc/AOS.cs b/Projects/UOContent/Misc/AOS.cs index 3a078d24e3..ec8c09b84f 100644 --- a/Projects/UOContent/Misc/AOS.cs +++ b/Projects/UOContent/Misc/AOS.cs @@ -1094,7 +1094,7 @@ public static int GetLowercaseLabel(SkillName skill) SkillName.AnimalTaming => 1049472, // animal taming SkillName.Macing => 1049470, // mace fighting SkillName.Necromancy => 1060842, // necromancy - SkillName.Focus => 1061616, // focus + SkillName.Focus => 1061613, // focus SkillName.Chivalry => 1061615, // chivalry SkillName.Bushido => 1062935, // bushido SkillName.Ninjitsu => 1062936, // ninjitsu @@ -1248,7 +1248,7 @@ public static void CheckCancelMorph(Mobile m) if (m.Skills.Ninjitsu.Value < AnimalForm.Entries[i].ReqSkill) { - AnimalForm.RemoveContext(m, true); + AnimalForm.RemoveContext(m); } } diff --git a/Projects/UOContent/Misc/AdminFirewall.cs b/Projects/UOContent/Misc/AdminFirewall.cs index 4f0ab02ae3..684546fba5 100644 --- a/Projects/UOContent/Misc/AdminFirewall.cs +++ b/Projects/UOContent/Misc/AdminFirewall.cs @@ -1,6 +1,5 @@ using System; using System.Buffers; -using System.Collections.Generic; using System.IO; using System.Net; using System.Runtime.CompilerServices; @@ -13,7 +12,6 @@ public static class AdminFirewall { private static readonly ILogger logger = LogFactory.GetLogger(typeof(AdminFirewall)); - private static readonly HashSet _firewallSet = []; private const string firewallConfigPath = "firewall.cfg"; public static void Configure() @@ -44,9 +42,6 @@ public static void Configure() } } - // Note: This is not optimized, so do not use this in hot paths - public static IReadOnlySet Set => _firewallSet; - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IFirewallEntry ToFirewallEntry(object entry) { @@ -91,41 +86,49 @@ public static IFirewallEntry ToFirewallEntry(string entry) } } - public static void Remove(object obj, bool save = true) + public static bool Remove(object obj, bool save = true) { var entry = ToFirewallEntry(obj); - if (entry != null) + if (entry == null) { - _firewallSet.Remove(entry); - Firewall.RequestRemoveEntry(entry); // Request that the TcpServer also remove the entry + return false; + } - if (save) - { - Save(); - } + if (!Firewall.Remove(entry)) + { + return false; + } + + if (save) + { + Save(); } + + return true; } - public static bool Add(object obj) => Add(ToFirewallEntry(obj)); + public static void Add(object obj) => Add(ToFirewallEntry(obj)); public static bool Add(IFirewallEntry entry, bool save = true) { - var added = _firewallSet.Add(entry); - Firewall.RequestAddEntry(entry); // Request that the TcpServer also add the entry + if (!Firewall.Add(entry)) + { + return false; + } if (save) { Save(); } - return added; + return true; } public static void Save() { using var op = new StreamWriter(firewallConfigPath); - foreach (var entry in Set) + foreach (var entry in Firewall.FirewallSet) { op.WriteLine(entry); } diff --git a/Projects/UOContent/Misc/Guild.cs b/Projects/UOContent/Misc/Guild.cs index ee6eaf04ba..3c9707160d 100644 --- a/Projects/UOContent/Misc/Guild.cs +++ b/Projects/UOContent/Misc/Guild.cs @@ -1050,7 +1050,7 @@ public void CheckExpiredWars() } } - public static void HandleDeath(Mobile victim, Mobile killer = null) + public static void HandleDeath(Mobile victim, Mobile killer) { if (!NewGuildSystem) { diff --git a/Projects/UOContent/Misc/ProfessionInfo.cs b/Projects/UOContent/Misc/ProfessionInfo.cs index 105927e204..ff644230ed 100644 --- a/Projects/UOContent/Misc/ProfessionInfo.cs +++ b/Projects/UOContent/Misc/ProfessionInfo.cs @@ -49,8 +49,6 @@ private static bool TryGetSkillName(string name, out SkillName skillName) static ProfessionInfo() { - List profs = []; - var file = Core.FindDataFile("prof.txt", false); if (!File.Exists(file)) { @@ -58,10 +56,11 @@ static ProfessionInfo() file = Path.Combine(ExpansionInfo.GetEraFolder(parent), "prof.txt"); } - var maxProf = 0; - if (File.Exists(file)) { + var maxProf = 0; + List profs = []; + using var s = File.OpenText(file); while (!s.EndOfStream) @@ -82,7 +81,6 @@ static ProfessionInfo() var prof = new ProfessionInfo(); - var statIndex = 0; var totalStats = 0; var skillIndex = 0; var totalSkill = 0; @@ -161,8 +159,8 @@ static ProfessionInfo() break; } - var skillValue = Utility.ToInt32(cols[2]); - prof.Skills[skillIndex++] = new SkillNameValue(skillName, skillValue); + var skillValue = byte.Parse(cols[2]); + prof.Skills[skillIndex++] = (skillName, skillValue); totalSkill += skillValue; } break; @@ -173,39 +171,44 @@ static ProfessionInfo() break; } - var statValue = Utility.ToInt32(cols[2]); - prof.Stats[statIndex++] = new StatNameValue(stat, statValue); + var statValue = byte.Parse(cols[2]); + prof.Stats[(int)stat >> 1] = statValue; totalStats += statValue; } break; } } } + + _professions = new ProfessionInfo[maxProf + 1]; + + foreach (var p in profs) + { + _professions[p.ID] = p; + } + + profs.Clear(); + profs.TrimExcess(); + } + else + { + _professions = new ProfessionInfo[1]; } - _professions = new ProfessionInfo[maxProf + 1]; _professions[0] = new ProfessionInfo { Name = "Advanced Skills" }; - - foreach (var p in profs) - { - _professions[p.ID] = p; - } - - profs.Clear(); - profs.TrimExcess(); } - private SkillNameValue[] _skills; + private (SkillName, byte)[] _skills; private ProfessionInfo() { Name = string.Empty; - _skills = new SkillNameValue[4]; - Stats = new StatNameValue[3]; + _skills = new (SkillName, byte)[4]; + Stats = new byte[3]; } public int ID { get; private set; } @@ -214,8 +217,8 @@ private ProfessionInfo() public int DescID { get; private set; } public bool TopLevel { get; private set; } public int GumpID { get; private set; } - public SkillNameValue[] Skills => _skills; - public StatNameValue[] Stats { get; } + public (SkillName, byte)[] Skills => _skills; + public byte[] Stats { get; } public void FixSkills() { @@ -223,7 +226,7 @@ public void FixSkills() while (index >= 0) { var skill = _skills[index]; - if (skill is not { Name: SkillName.Alchemy, Value: 0 }) + if (skill is not (SkillName.Alchemy, 0)) { break; } diff --git a/Projects/UOContent/Misc/ServerList.cs b/Projects/UOContent/Misc/ServerList.cs index e208f7c9b5..56cbaad2a0 100644 --- a/Projects/UOContent/Misc/ServerList.cs +++ b/Projects/UOContent/Misc/ServerList.cs @@ -3,6 +3,7 @@ using System.Net.Http; using System.Net.NetworkInformation; using System.Net.Sockets; +using ModernUO.CodeGeneratedEvents; using Server.Logging; using Server.Network; @@ -40,6 +41,8 @@ public static class ServerList public static bool AutoDetect { get; private set; } + private static bool _useServerListingAddressConfig { get; set; } + public static void Configure() { Address = ServerConfiguration.GetOrUpdateSetting("serverListing.address", null); @@ -59,12 +62,18 @@ public static void Initialize() else { Resolve(Address, out _publicAddress); - } - EventSink.ServerList += EventSink_ServerList; + if (_publicAddress != null) + { + _useServerListingAddressConfig = true; + + logger.Information("Server listing address set from config: {address}", _publicAddress); + } + } } - private static void EventSink_ServerList(ServerListEventArgs e) + [OnEvent(nameof(GatewayServer.ServerListEvent))] + public static void OnServerListEvent(GatewayServer.ServerListEventArgs e) { try { @@ -79,9 +88,14 @@ private static void EventSink_ServerList(ServerListEventArgs e) var localAddress = ipep.Address; var localPort = ipep.Port; - if (IsPrivateNetwork(localAddress)) + if (_useServerListingAddressConfig) + { + localAddress = _publicAddress; + } + else if (IsPrivateNetwork(localAddress)) { ipep = (IPEndPoint)ns.Connection.RemoteEndPoint; + if (ipep == null || !IsPrivateNetwork(ipep.Address) && _publicAddress != null) { localAddress = _publicAddress; diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 2cc3adea1d..b6558209a7 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using ModernUO.CodeGeneratedEvents; using Server.Collections; using Server.ContextMenus; using Server.Engines.ConPVP; @@ -155,7 +156,7 @@ public static TextDefinition GetFriendlyNameFor(Type t) } } - public abstract class BaseCreature : Mobile, IHonorTarget, IQuestGiver + public abstract partial class BaseCreature : Mobile, IHonorTarget, IQuestGiver { public enum Allegiance { @@ -3248,10 +3249,11 @@ public virtual void OnKilledBy(Mobile mob) } } + [GeneratedEvent(nameof(CreatureDeathEvent))] + public static partial void CreatureDeathEvent(Mobile m); + public override void OnDeath(Container c) { - MeerMage.StopEffect(this, false); - if (IsBonded) { Effects.PlaySound(this, GetDeathSound()); @@ -3312,7 +3314,7 @@ public override void OnDeath(Container c) OwnerAbandonTime = DateTime.MinValue; } - GiftOfLifeSpell.HandleDeath(this); + CreatureDeathEvent(this); CheckStatTimers(); return; @@ -3435,6 +3437,8 @@ public override void OnDeath(Container c) { c.Delete(); } + + CreatureDeathEvent(this); } public override void OnDelete() diff --git a/Projects/UOContent/Mobiles/Healers/PricedHealer.cs b/Projects/UOContent/Mobiles/Healers/PricedHealer.cs index fdf127374e..c214a8cab5 100644 --- a/Projects/UOContent/Mobiles/Healers/PricedHealer.cs +++ b/Projects/UOContent/Mobiles/Healers/PricedHealer.cs @@ -1,5 +1,4 @@ using ModernUO.Serialization; -using Server.Gumps; namespace Server.Mobiles; diff --git a/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/KhaldunRevenant.cs b/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/KhaldunRevenant.cs index 4de259a0ac..f8e3ea8a77 100644 --- a/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/KhaldunRevenant.cs +++ b/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/KhaldunRevenant.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using ModernUO.Serialization; using Server.Items; @@ -65,12 +66,8 @@ public KhaldunRevenant(Mobile target) : base(AIType.AI_Melee) public override bool BardImmune => true; public override Poison PoisonImmune => Poison.Lethal; - public new static void Initialize() - { - EventSink.PlayerDeath += EventSink_PlayerDeath; - } - - public static void EventSink_PlayerDeath(Mobile m) + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + public static void OnPlayerDeathEvent(Mobile m) { var lastKiller = m.LastKiller; diff --git a/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/OrcBrute.cs b/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/OrcBrute.cs index 62387a71a9..45541153c6 100644 --- a/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/OrcBrute.cs +++ b/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/OrcBrute.cs @@ -102,7 +102,7 @@ public void SpawnOrcLord(Mobile target) { var map = target.Map; - if (map == null) + if (map == null || map == Map.Internal) { return; } @@ -112,14 +112,20 @@ public void SpawnOrcLord(Mobile target) { if (++count == 10) { - BaseCreature orc = new SpawnedOrcishLord { Team = Team }; - - // This MoveToWorld is safe since we return after executing and do not keep looping. - orc.MoveToWorld(map.GetRandomNearbyLocation(target.Location), map); - orc.Combatant = target; return; } } + + var location = map.GetRandomNearbyLocation(target.Location); + var orc = new SpawnedOrcishLord + { + Team = Team, + Home = location, + RangeHome = 10, + Combatant = target + }; + + orc.MoveToWorld(location, map); } } } diff --git a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs index c34c9b9ab4..178436fb9b 100644 --- a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs +++ b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs @@ -1,6 +1,7 @@ using ModernUO.Serialization; using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Items; namespace Server.Mobiles @@ -152,7 +153,9 @@ out var timerToken public static bool UnderEffect(Mobile m) => m_Table.ContainsKey(m); - public static void StopEffect(Mobile m, bool message) + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + [OnEvent(nameof(CreatureDeathEvent))] + public static void StopEffect(Mobile m, bool message = false) { if (m_Table.Remove(m, out var timer)) { @@ -170,7 +173,7 @@ private void DoEffect(Mobile m, int count) { if (!m.Alive) { - StopEffect(m, false); + StopEffect(m); return; } @@ -197,7 +200,7 @@ private void DoEffect(Mobile m, int count) if (!m.Alive) { - StopEffect(m, false); + StopEffect(m); } } } diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index b88f3a9fb4..f9b607cc99 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Accounting; using Server.Collections; using Server.ContextMenus; @@ -34,7 +35,6 @@ using Server.Spells.Necromancy; using Server.Spells.Ninjitsu; using Server.Spells.Second; -using Server.Spells.Seventh; using Server.Spells.Sixth; using Server.Spells.Spellweaving; using Server.Targeting; @@ -97,7 +97,7 @@ public enum BlockMountType DismountRecovery = 1070859 // You cannot mount while recovering from a dismount special maneuver. } - public class PlayerMobile : Mobile, IHonorTarget, IHasSteps + public partial class PlayerMobile : Mobile, IHonorTarget, IHasSteps { private static bool m_NoRecursion; @@ -1089,7 +1089,7 @@ public void SetMountBlock(BlockMountType type, TimeSpan duration, bool dismount) } else if (AnimalForm.UnderTransformation(this)) { - AnimalForm.RemoveContext(this, true); + AnimalForm.RemoveContext(this); } } @@ -1327,32 +1327,13 @@ private void ValidateEquipment_Sandbox() } var item = items[i]; + var itemEthic = Ethic.Find(item); - if ((item.SavedFlags & 0x100) != 0) + if (itemEthic != null && itemEthic != ethic) { - if (item.Hue != Ethic.Hero.Definition.PrimaryHue) - { - item.SavedFlags &= ~0x100; - } - else if (ethic != Ethic.Hero) - { - from.AddToBackpack(item); - moved = true; - continue; - } - } - else if ((item.SavedFlags & 0x200) != 0) - { - if (item.Hue != Ethic.Evil.Definition.PrimaryHue) - { - item.SavedFlags &= ~0x200; - } - else if (ethic != Ethic.Evil) - { - from.AddToBackpack(item); - moved = true; - continue; - } + from.AddToBackpack(item); + moved = true; + continue; } if (item is BaseWeapon weapon) @@ -2549,6 +2530,9 @@ public override DeathMoveResult GetInventoryMoveResultFor(Item item) return res; } + [GeneratedEvent(nameof(PlayerDeathEvent))] + public static partial void PlayerDeathEvent(PlayerMobile m); + public override void OnDeath(Container c) { if (m_NonAutoreinsuredItems > 0) @@ -2563,27 +2547,14 @@ public override void OnDeath(Container c) HueMod = -1; NameMod = null; SavagePaintExpiration = TimeSpan.Zero; - SetHairMods(-1, -1); - PolymorphSpell.StopTimer(this); - IncognitoSpell.StopTimer(this); - DisguisePersistence.RemoveTimer(this); - AnimalForm.RemoveContext(this, true); - - EndAction(); - EndAction(); - - MeerMage.StopEffect(this, false); - if (Flying) { Flying = false; BuffInfo.RemoveBuff(this, BuffIcon.Fly); } - StolenItem.ReturnOnDeath(this, c); - if (PermaFlags.Count > 0) { PermaFlags.Clear(); @@ -2669,10 +2640,6 @@ public override void OnDeath(Container c) Guilds.Guild.HandleDeath(this, killer); - MLQuestSystem.HandleDeath(this); - - DuelContext?.OnDeath(this, c); - if (m_BuffTable != null) { using var queue = PooledRefQueue.Create(); @@ -2690,6 +2657,8 @@ public override void OnDeath(Container c) RemoveBuff(queue.Dequeue()); } } + + PlayerDeathEvent(this); } public override bool MutateSpeech(List hears, ref string text, ref object context) diff --git a/Projects/UOContent/Mobiles/Vendors/VendorItem.cs b/Projects/UOContent/Mobiles/Vendors/VendorItem.cs index 6ff8482138..14af3656aa 100644 --- a/Projects/UOContent/Mobiles/Vendors/VendorItem.cs +++ b/Projects/UOContent/Mobiles/Vendors/VendorItem.cs @@ -60,5 +60,7 @@ private void AfterDeserialization() { _price = 100000000; } + + Valid = true; } } diff --git a/Projects/UOContent/Network/GameServer.cs b/Projects/UOContent/Network/GameServer.cs new file mode 100644 index 0000000000..9fc170d692 --- /dev/null +++ b/Projects/UOContent/Network/GameServer.cs @@ -0,0 +1,29 @@ +using ModernUO.CodeGeneratedEvents; + +namespace Server.Network; + +public static partial class GameServer +{ + public class GameLoginEventArgs + { + public GameLoginEventArgs(NetState state, string un, string pw) + { + State = state; + Username = un; + Password = pw; + } + + public NetState State { get; } + + public string Username { get; } + + public string Password { get; } + + public bool Accepted { get; set; } + + public CityInfo[] CityInfo { get; set; } + } + + [GeneratedEvent(nameof(GameServerLoginEvent))] + public static partial void GameServerLoginEvent(GameLoginEventArgs e); +} diff --git a/Projects/UOContent/Network/GatewayServer.cs b/Projects/UOContent/Network/GatewayServer.cs new file mode 100644 index 0000000000..6c9c143a39 --- /dev/null +++ b/Projects/UOContent/Network/GatewayServer.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Net; +using ModernUO.CodeGeneratedEvents; +using Server.Accounting; + +namespace Server.Network; + +public static partial class GatewayServer +{ + public class ServerListEventArgs + { + public ServerListEventArgs(NetState state, IAccount account) + { + State = state; + Account = account; + Servers = []; + } + + public NetState State { get; } + + public IAccount Account { get; } + + public bool Rejected { get; set; } + + public List Servers { get; } + + public void AddServer(string name, IPEndPoint address) => AddServer(name, 0, TimeZoneInfo.Local, address); + + public void AddServer(string name, int fullPercent, TimeZoneInfo tz, IPEndPoint address) => + Servers.Add(new ServerInfo(name, fullPercent, tz, address)); + } + + [GeneratedEvent(nameof(ServerListEvent))] + public static partial void ServerListEvent(ServerListEventArgs e); +} diff --git a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs index eb8a89e830..099072b36f 100644 --- a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs @@ -20,6 +20,7 @@ using Server.Accounting; using Server.Assistants; using Server.Commands; +using Server.Engines.CharacterCreation; using Server.Engines.ConPVP; using Server.Engines.Doom; using Server.Engines.PartySystem; @@ -33,7 +34,6 @@ using Server.Regions; using Server.Spells.Ninjitsu; using Server.Spells.Spellweaving; -using CV = Server.ClientVersion; namespace Server.Network; @@ -64,7 +64,6 @@ public static unsafe void Configure() IncomingPackets.Register(0x91, &GameLogin, 65, outgameOnly: true); IncomingPackets.Register(0xA0, &PlayServer, 3, outgameOnly: true); IncomingPackets.Register(0xBD, &ClientVersion); - IncomingPackets.Register(0xCF, &AccountLogin, outgameOnly: true); IncomingPackets.Register(0xE1, &ClientType); IncomingPackets.Register(0xEF, &LoginServerSeed, 21, outgameOnly: true); IncomingPackets.Register(0xF8, &CreateCharacter, 106, outgameOnly: true); @@ -88,20 +87,17 @@ public static void CreateCharacter(NetState state, SpanReader reader) var genderRace = reader.ReadByte(); - var stats = new StatNameValue[] - { - new(StatType.Str, reader.ReadByte()), - new(StatType.Dex, reader.ReadByte()), - new(StatType.Int, reader.ReadByte()) - }; - - var skills = new SkillNameValue[state.NewCharacterCreation ? 4 : 3]; - skills[0] = new SkillNameValue((SkillName)reader.ReadByte(), reader.ReadByte()); - skills[1] = new SkillNameValue((SkillName)reader.ReadByte(), reader.ReadByte()); - skills[2] = new SkillNameValue((SkillName)reader.ReadByte(), reader.ReadByte()); + // Strength, Dex, Intelligence + byte[] stats = [reader.ReadByte(), reader.ReadByte(), reader.ReadByte()]; + + var skills = new (SkillName, byte)[state.NewCharacterCreation ? 4 : 3]; + skills[0] = ((SkillName)reader.ReadByte(), reader.ReadByte()); + skills[1] = ((SkillName)reader.ReadByte(), reader.ReadByte()); + skills[2] = ((SkillName)reader.ReadByte(), reader.ReadByte()); + if (state.NewCharacterCreation) { - skills[3] = new SkillNameValue((SkillName)reader.ReadByte(), reader.ReadByte()); + skills[3] = ((SkillName)reader.ReadByte(), reader.ReadByte()); } int hue = reader.ReadUInt16(); @@ -183,7 +179,7 @@ public static void CreateCharacter(NetState state, SpanReader reader) state.BlockAllPackets = true; - EventSink.InvokeCharacterCreated(args); + CharacterCreation.CharacterCreatedEvent(args); var m = args.Mobile; @@ -208,13 +204,9 @@ public static void DeleteCharacter(NetState state, SpanReader reader) AccountHandler.DeleteRequest(state, index); } - public static void AccountID(NetState state, SpanReader reader) - { - } - public static void ClientVersion(NetState state, SpanReader reader) { - var version = state.Version = new CV(reader.ReadAscii()); + var version = state.Version = new ClientVersion(reader.ReadAscii()); ClientVerification.ClientVersionReceived(state, version); } @@ -224,7 +216,7 @@ public static void ClientType(NetState state, SpanReader reader) reader.ReadUInt16(); int type = reader.ReadUInt16(); - var version = state.Version = new CV(reader.ReadAscii()); + var version = state.Version = new ClientVersion(reader.ReadAscii()); ClientVerification.ClientVersionReceived(state, version); } @@ -420,9 +412,9 @@ public static void GameLogin(NetState state, SpanReader reader) var username = reader.ReadAscii(30); var password = reader.ReadAscii(30); - var e = new GameLoginEventArgs(state, username, password); + var e = new GameServer.GameLoginEventArgs(state, username, password); - EventSink.InvokeGameLogin(e); + GameServer.GameServerLoginEvent(e); if (e.Accepted) { @@ -501,9 +493,9 @@ public static void AccountLogin(NetState state, SpanReader reader) if (accountLoginEventArgs.Accepted) { - var serverListEventArgs = new ServerListEventArgs(state, state.Account); + var serverListEventArgs = new GatewayServer.ServerListEventArgs(state, state.Account); - EventSink.InvokeServerList(serverListEventArgs); + GatewayServer.ServerListEvent(serverListEventArgs); if (serverListEventArgs.Rejected) { diff --git a/Projects/UOContent/Network/Packets/IncomingExtendedCommandPackets.cs b/Projects/UOContent/Network/Packets/IncomingExtendedCommandPackets.cs index 00216857cc..c593a56a23 100644 --- a/Projects/UOContent/Network/Packets/IncomingExtendedCommandPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingExtendedCommandPackets.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: IncomingExtendedCommandPackets.cs * * * @@ -15,12 +15,13 @@ using System.Buffers; using System.Runtime.CompilerServices; +using ModernUO.CodeGeneratedEvents; using Server.Items; using Server.Mobiles; namespace Server.Network; -public static class IncomingExtendedCommandPackets +public static partial class IncomingExtendedCommandPackets { private static readonly PacketHandler[] _extendedHandlers = new PacketHandler[0x100]; @@ -432,13 +433,16 @@ public static void TargetedSkillUse(NetState state, SpanReader reader) PlayerMobile.TargetedSkillUse(state.Mobile, World.FindEntity((Serial)reader.ReadUInt32()), skillId); } + [GeneratedEvent("TargetByResourceMacro")] + public static partial void InvokeTargetByResourceMacro(Mobile m, Item item, short resourceType); + public static void TargetByResourceMacro(NetState state, SpanReader reader) { var serial = (Serial)reader.ReadUInt32(); if (serial.IsItem) { - EventSink.InvokeTargetByResourceMacro(state.Mobile, World.FindItem(serial), reader.ReadInt16()); + InvokeTargetByResourceMacro(state.Mobile, World.FindItem(serial), reader.ReadInt16()); } } } diff --git a/Projects/UOContent/Network/Packets/IncomingTargetingPackets.cs b/Projects/UOContent/Network/Packets/IncomingTargetingPackets.cs index dad01751d2..9d68bccf87 100644 --- a/Projects/UOContent/Network/Packets/IncomingTargetingPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingTargetingPackets.cs @@ -14,7 +14,6 @@ *************************************************************************/ using System.Buffers; -using Server.Diagnostics; using Server.Targeting; namespace Server.Network; @@ -52,95 +51,83 @@ public static void TargetResponse(NetState state, SpanReader reader) return; } - var prof = TargetProfile.Acquire(t.GetType()); - prof?.Start(); - - try + if (x == -1 && y == -1 && !serial.IsValid) { - if (x == -1 && y == -1 && !serial.IsValid) - { - // User pressed escape - t.Cancel(from, TargetCancelType.Canceled); - } - else if (t.TargetID == targetID) - { - object toTarget; + // User pressed escape + t.Cancel(from, TargetCancelType.Canceled); + } + else if (t.TargetID == targetID) + { + object toTarget; - if (type == 1) + if (type == 1) + { + if (graphic == 0) + { + toTarget = new LandTarget(new Point3D(x, y, z), from.Map); + } + else { - if (graphic == 0) + var map = from.Map; + + if (map == null || map == Map.Internal) { - toTarget = new LandTarget(new Point3D(x, y, z), from.Map); + t.Cancel(from, TargetCancelType.Canceled); + return; } else { - var map = from.Map; - - if (map == null || map == Map.Internal) - { - t.Cancel(from, TargetCancelType.Canceled); - return; - } - else + if (state.HighSeas) { - if (state.HighSeas) + var id = TileData.ItemTable[graphic & TileData.MaxItemValue]; + if (id.Surface) { - var id = TileData.ItemTable[graphic & TileData.MaxItemValue]; - if (id.Surface) - { - z -= id.Height; - } + z -= id.Height; } + } - int hue = 0; - var valid = false; + int hue = 0; + var valid = false; - var eable = t.DisallowMultis - ? map.Tiles.GetStaticTiles(x, y) - : map.Tiles.GetStaticAndMultiTiles(x, y); + var eable = t.DisallowMultis + ? map.Tiles.GetStaticTiles(x, y) + : map.Tiles.GetStaticAndMultiTiles(x, y); - foreach (var tile in eable) + foreach (var tile in eable) + { + if (tile.Z == z && tile.ID == graphic) { - if (tile.Z == z && tile.ID == graphic) - { - valid = true; - hue = tile.Hue; - break; - } + valid = true; + hue = tile.Hue; + break; } + } - if (!valid) - { - t.Cancel(from, TargetCancelType.Canceled); - return; - } - else - { - toTarget = new StaticTarget(new Point3D(x, y, z), graphic, hue); - } + if (!valid) + { + t.Cancel(from, TargetCancelType.Canceled); + return; } + + toTarget = new StaticTarget(new Point3D(x, y, z), graphic, hue); } } - else if (serial.IsMobile) - { - toTarget = World.FindMobile(serial); - } - else if (serial.IsItem) - { - toTarget = World.FindItem(serial); - } - else - { - t.Cancel(from, TargetCancelType.Canceled); - return; - } - - t.Invoke(from, toTarget); } - } - finally - { - prof?.Finish(); + else if (serial.IsMobile) + { + toTarget = World.FindMobile(serial); + } + else if (serial.IsItem) + { + toTarget = World.FindItem(serial); + } + else + { + t.Cancel(from, TargetCancelType.Canceled); + return; + } + + t.Invoke(from, toTarget); } } } diff --git a/Projects/UOContent/Skills/Stealing.cs b/Projects/UOContent/Skills/Stealing.cs index 760dfed43c..c0459a6215 100644 --- a/Projects/UOContent/Skills/Stealing.cs +++ b/Projects/UOContent/Skills/Stealing.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Engines.ConPVP; using Server.Engines.Stealables; using Server.Factions; @@ -465,10 +466,13 @@ public static bool IsStolen(Item item, ref Mobile victim) return false; } - public static void ReturnOnDeath(Mobile killed, Container corpse) + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + public static void ReturnOnDeath(Mobile killed) { Clean(); + var corpse = killed.Corpse; + foreach (var si in m_Queue) { if (si.Stolen.RootParent == corpse && si.Victim != null && !si.IsExpired) diff --git a/Projects/UOContent/Spells/Eighth/EnergyVortex.cs b/Projects/UOContent/Spells/Eighth/EnergyVortex.cs index bd5ee1c6c7..2269e471e9 100644 --- a/Projects/UOContent/Spells/Eighth/EnergyVortex.cs +++ b/Projects/UOContent/Spells/Eighth/EnergyVortex.cs @@ -60,7 +60,7 @@ public override bool CheckCast() public override void OnCast() { - Caster.Target = new SpellTarget(this, retryOnLos: true); + Caster.Target = new SpellTarget(this, allowGround: true, retryOnLos: true); } } } diff --git a/Projects/UOContent/Spells/Fifth/BladeSpirits.cs b/Projects/UOContent/Spells/Fifth/BladeSpirits.cs index f0b3e56cc3..d44d1668fd 100644 --- a/Projects/UOContent/Spells/Fifth/BladeSpirits.cs +++ b/Projects/UOContent/Spells/Fifth/BladeSpirits.cs @@ -78,7 +78,7 @@ public override bool CheckCast() public override void OnCast() { - Caster.Target = new SpellTarget(this, retryOnLos: true); + Caster.Target = new SpellTarget(this, allowGround: true, retryOnLos: true); } } } diff --git a/Projects/UOContent/Spells/Fifth/Incognito.cs b/Projects/UOContent/Spells/Fifth/Incognito.cs index 68797a6566..a1092a3071 100644 --- a/Projects/UOContent/Spells/Fifth/Incognito.cs +++ b/Projects/UOContent/Spells/Fifth/Incognito.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Factions; using Server.Items; using Server.Mobiles; @@ -124,6 +125,13 @@ public override void OnCast() FinishSequence(); } + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + public static void OnPlayerDeathEvent(Mobile m) + { + StopTimer(m); + m.EndAction(); + } + public static void StopTimer(Mobile m) { if (_table.Remove(m, out var timerToken)) diff --git a/Projects/UOContent/Spells/Fifth/PoisonField.cs b/Projects/UOContent/Spells/Fifth/PoisonField.cs index 6c550ef6d1..7f38c677f2 100644 --- a/Projects/UOContent/Spells/Fifth/PoisonField.cs +++ b/Projects/UOContent/Spells/Fifth/PoisonField.cs @@ -57,7 +57,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } diff --git a/Projects/UOContent/Spells/Fourth/ArchCure.cs b/Projects/UOContent/Spells/Fourth/ArchCure.cs index 9531903c12..ed5307df3e 100644 --- a/Projects/UOContent/Spells/Fourth/ArchCure.cs +++ b/Projects/UOContent/Spells/Fourth/ArchCure.cs @@ -95,7 +95,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } private bool AreaCanTarget(Mobile target, bool feluccaRules) diff --git a/Projects/UOContent/Spells/Fourth/ArchProtection.cs b/Projects/UOContent/Spells/Fourth/ArchProtection.cs index cb156635df..18e2ab58bd 100644 --- a/Projects/UOContent/Spells/Fourth/ArchProtection.cs +++ b/Projects/UOContent/Spells/Fourth/ArchProtection.cs @@ -94,7 +94,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } private static void AddEntry(Mobile m, int v) diff --git a/Projects/UOContent/Spells/Fourth/FireField.cs b/Projects/UOContent/Spells/Fourth/FireField.cs index 4632edd1aa..004a43b574 100644 --- a/Projects/UOContent/Spells/Fourth/FireField.cs +++ b/Projects/UOContent/Spells/Fourth/FireField.cs @@ -60,7 +60,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } diff --git a/Projects/UOContent/Spells/Mysticism/AnimatedWeaponSpell.cs b/Projects/UOContent/Spells/Mysticism/AnimatedWeaponSpell.cs index 6b5ed7bdb1..6775ef8ea9 100644 --- a/Projects/UOContent/Spells/Mysticism/AnimatedWeaponSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/AnimatedWeaponSpell.cs @@ -55,6 +55,6 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } diff --git a/Projects/UOContent/Spells/Mysticism/HailStormSpell.cs b/Projects/UOContent/Spells/Mysticism/HailStormSpell.cs index febd11f0bf..cf21bd7506 100644 --- a/Projects/UOContent/Spells/Mysticism/HailStormSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/HailStormSpell.cs @@ -73,7 +73,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } private static void PlayEffect(Point3D p, Map map) diff --git a/Projects/UOContent/Spells/Mysticism/NetherCycloneSpell.cs b/Projects/UOContent/Spells/Mysticism/NetherCycloneSpell.cs index 71ac15a969..04e7a316a1 100644 --- a/Projects/UOContent/Spells/Mysticism/NetherCycloneSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/NetherCycloneSpell.cs @@ -89,7 +89,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } private static void PlayEffect(Point3D p, Map map) diff --git a/Projects/UOContent/Spells/Mysticism/SpellPlagueSpell.cs b/Projects/UOContent/Spells/Mysticism/SpellPlagueSpell.cs index 360dad7731..f1ed5d6751 100644 --- a/Projects/UOContent/Spells/Mysticism/SpellPlagueSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/SpellPlagueSpell.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; +using Server.Mobiles; using Server.Targeting; namespace Server.Spells.Mysticism; @@ -25,11 +27,6 @@ public SpellPlagueSpell(Mobile caster, Item scroll = null) : base(caster, scroll public override SpellCircle Circle => SpellCircle.Seventh; - public static void Initialize() - { - EventSink.PlayerDeath += OnPlayerDeath; - } - public override void OnCast() { Caster.Target = new SpellTarget(this, TargetFlags.Harmful); @@ -75,6 +72,7 @@ public void Target(Mobile m) public static bool UnderEffect(Mobile m) => _table.ContainsKey(m); + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] public static bool RemoveEffect(Mobile m) { if (_table.Remove(m, out var timer)) @@ -95,8 +93,6 @@ public static void OnMobileDamaged(Mobile m) } } - private static void OnPlayerDeath(Mobile m) => RemoveEffect(m); - private static void VisualEffect(Mobile to) { to.PlaySound(0x658); diff --git a/Projects/UOContent/Spells/Mysticism/StoneFormSpell.cs b/Projects/UOContent/Spells/Mysticism/StoneFormSpell.cs index 3648abb85b..1820fecd08 100644 --- a/Projects/UOContent/Spells/Mysticism/StoneFormSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/StoneFormSpell.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Factions; +using Server.Mobiles; using Server.Spells.Fifth; using Server.Spells.Ninjitsu; using Server.Spells.Seventh; @@ -26,11 +28,6 @@ public StoneFormSpell(Mobile caster, Item scroll = null) : base(caster, scroll, public override SpellCircle Circle => SpellCircle.Fourth; - public static void Initialize() - { - EventSink.PlayerDeath += OnPlayerDeath; - } - public static bool UnderEffect(Mobile m) => _table.ContainsKey(m); public override bool CheckCast() @@ -137,6 +134,7 @@ public override void OnCast() FinishSequence(); } + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] public static void RemoveEffects(Mobile m) { if (!_table.Remove(m, out var mods)) @@ -154,10 +152,5 @@ public static void RemoveEffects(Mobile m) BuffInfo.RemoveBuff(m, BuffIcon.StoneForm); } - - private static void OnPlayerDeath(Mobile m) - { - RemoveEffects(m); - } } } diff --git a/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs b/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs index c4588e4d8e..857fea7010 100644 --- a/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs +++ b/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Gumps; using Server.Items; using Server.Mobiles; @@ -135,7 +136,7 @@ public override void OnCast() } else if (context != null) { - RemoveContext(Caster, context, true); + RemoveContext(Caster, context); Caster.Mana -= mana; } else @@ -254,17 +255,18 @@ public static void AddContext(Mobile m, AnimalFormContext context) } } - public static void RemoveContext(Mobile m, bool resetGraphics) + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + public static void RemoveContext(Mobile m) { var context = GetContext(m); if (context != null) { - RemoveContext(m, context, resetGraphics); + RemoveContext(m, context); } } - public static void RemoveContext(Mobile m, AnimalFormContext context, bool resetGraphics) + public static void RemoveContext(Mobile m, AnimalFormContext context) { _table.Remove(m); @@ -287,11 +289,8 @@ public static void RemoveContext(Mobile m, AnimalFormContext context, bool reset m.RemoveSkillMod(mod); } - if (resetGraphics) - { - m.HueMod = -1; - m.BodyMod = 0; - } + m.HueMod = -1; + m.BodyMod = 0; m.FixedParticles(0x3728, 10, 13, 2023, EffectLayer.Waist); @@ -517,7 +516,7 @@ protected override void OnTick() { if (_mobile.Deleted || !_mobile.Alive || _mobile.Body != _body || _mobile.Hue != _hue) { - AnimalForm.RemoveContext(_mobile, true); + AnimalForm.RemoveContext(_mobile); Stop(); return; } diff --git a/Projects/UOContent/Spells/Ninjitsu/ShadowJump.cs b/Projects/UOContent/Spells/Ninjitsu/ShadowJump.cs index c281e12990..65fe528938 100644 --- a/Projects/UOContent/Spells/Ninjitsu/ShadowJump.cs +++ b/Projects/UOContent/Spells/Ninjitsu/ShadowJump.cs @@ -112,6 +112,6 @@ public override bool CheckCast() public override void OnCast() { Caster.SendLocalizedMessage(1063088); // You prepare to perform a Shadowjump. - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } diff --git a/Projects/UOContent/Spells/Seventh/ChainLightning.cs b/Projects/UOContent/Spells/Seventh/ChainLightning.cs index e033d6890d..cb8fe203ee 100644 --- a/Projects/UOContent/Spells/Seventh/ChainLightning.cs +++ b/Projects/UOContent/Spells/Seventh/ChainLightning.cs @@ -99,7 +99,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } } diff --git a/Projects/UOContent/Spells/Seventh/EnergyField.cs b/Projects/UOContent/Spells/Seventh/EnergyField.cs index 9d079f5ab2..87dedeaf76 100644 --- a/Projects/UOContent/Spells/Seventh/EnergyField.cs +++ b/Projects/UOContent/Spells/Seventh/EnergyField.cs @@ -71,7 +71,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } diff --git a/Projects/UOContent/Spells/Seventh/MassDispel.cs b/Projects/UOContent/Spells/Seventh/MassDispel.cs index 3f1b0c8e56..bd5113e084 100644 --- a/Projects/UOContent/Spells/Seventh/MassDispel.cs +++ b/Projects/UOContent/Spells/Seventh/MassDispel.cs @@ -76,7 +76,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } } diff --git a/Projects/UOContent/Spells/Seventh/MeteorSwarm.cs b/Projects/UOContent/Spells/Seventh/MeteorSwarm.cs index fd1ba62824..adb35a8270 100644 --- a/Projects/UOContent/Spells/Seventh/MeteorSwarm.cs +++ b/Projects/UOContent/Spells/Seventh/MeteorSwarm.cs @@ -104,7 +104,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } } diff --git a/Projects/UOContent/Spells/Seventh/Polymorph.cs b/Projects/UOContent/Spells/Seventh/Polymorph.cs index 4505de7cc6..448b3f95c2 100644 --- a/Projects/UOContent/Spells/Seventh/Polymorph.cs +++ b/Projects/UOContent/Spells/Seventh/Polymorph.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Factions; using Server.Gumps; using Server.Items; +using Server.Mobiles; using Server.Spells.Fifth; namespace Server.Spells.Seventh @@ -184,6 +186,13 @@ public override void OnCast() FinishSequence(); } + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + public static void OnPlayerDeathEvent(Mobile m) + { + StopTimer(m); + m.EndAction(); + } + public static void StopTimer(Mobile m) { if (_table.Remove(m, out var timer)) diff --git a/Projects/UOContent/Spells/Sixth/Explosion.cs b/Projects/UOContent/Spells/Sixth/Explosion.cs index 687d2a7f44..f6f49ce0ea 100644 --- a/Projects/UOContent/Spells/Sixth/Explosion.cs +++ b/Projects/UOContent/Spells/Sixth/Explosion.cs @@ -68,6 +68,11 @@ public InternalTimer(MagerySpell spell, Mobile attacker, Mobile defender, Mobile protected override void OnTick() { + if (_defender.Deleted || !_defender.Alive) + { + return; + } + if (_attacker.HarmfulCheck(_defender)) { double damage; diff --git a/Projects/UOContent/Spells/Sixth/MassCurse.cs b/Projects/UOContent/Spells/Sixth/MassCurse.cs index 5f38e75474..a0466ddba3 100644 --- a/Projects/UOContent/Spells/Sixth/MassCurse.cs +++ b/Projects/UOContent/Spells/Sixth/MassCurse.cs @@ -58,7 +58,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } } diff --git a/Projects/UOContent/Spells/Sixth/ParalyzeField.cs b/Projects/UOContent/Spells/Sixth/ParalyzeField.cs index e05009dea7..ce2ec48f05 100644 --- a/Projects/UOContent/Spells/Sixth/ParalyzeField.cs +++ b/Projects/UOContent/Spells/Sixth/ParalyzeField.cs @@ -71,7 +71,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } diff --git a/Projects/UOContent/Spells/Sixth/Reveal.cs b/Projects/UOContent/Spells/Sixth/Reveal.cs index 73a9ce2960..d4db755d6d 100644 --- a/Projects/UOContent/Spells/Sixth/Reveal.cs +++ b/Projects/UOContent/Spells/Sixth/Reveal.cs @@ -55,7 +55,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } // Reveal uses magery and detect hidden vs. hide and stealth diff --git a/Projects/UOContent/Spells/Spellweaving/GiftOfLife.cs b/Projects/UOContent/Spells/Spellweaving/GiftOfLife.cs index 320fcc3fea..7118854b71 100644 --- a/Projects/UOContent/Spells/Spellweaving/GiftOfLife.cs +++ b/Projects/UOContent/Spells/Spellweaving/GiftOfLife.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; using Server.Gumps; using Server.Mobiles; using Server.Targeting; @@ -70,17 +71,14 @@ public void Target(Mobile m) } } - public static void Initialize() - { - EventSink.PlayerDeath += HandleDeath; - } - public override void OnCast() { Caster.Target = new SpellTarget(this, TargetFlags.Beneficial); } - public static void HandleDeath(Mobile m) + [OnEvent(nameof(BaseCreature.CreatureDeathEvent))] + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + public static void OnDeathEvent(Mobile m) { if (_table.ContainsKey(m)) { @@ -130,16 +128,6 @@ private static void HandleDeath_OnCallback(Mobile m) timer.DoExpire(); } - public static void OnLogin(Mobile m) - { - if (m?.Alive != false || _table[m] == null) - { - return; - } - - HandleDeath_OnCallback(m); - } - private class ExpireTimer : Timer { private Mobile _mobile; diff --git a/Projects/UOContent/Spells/Spellweaving/NatureFury.cs b/Projects/UOContent/Spells/Spellweaving/NatureFury.cs index 2527d2a934..144e3b735b 100644 --- a/Projects/UOContent/Spells/Spellweaving/NatureFury.cs +++ b/Projects/UOContent/Spells/Spellweaving/NatureFury.cs @@ -70,7 +70,7 @@ public override bool CheckCast() public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } private class InternalTimer : Timer diff --git a/Projects/UOContent/Spells/Targeting/SpellTarget.cs b/Projects/UOContent/Spells/Targeting/SpellTarget.cs index ccc9be5207..74a12d8c15 100644 --- a/Projects/UOContent/Spells/Targeting/SpellTarget.cs +++ b/Projects/UOContent/Spells/Targeting/SpellTarget.cs @@ -13,14 +13,18 @@ public class SpellTarget : Target, ISpellTarget where T : class, IPoint3D public SpellTarget( ITargetingSpell spell, - TargetFlags flags = TargetFlags.None, + TargetFlags flags, bool retryOnLos = false ) : this(spell, false, flags, retryOnLos) { } - public SpellTarget(ITargetingSpell spell, bool allowGround, TargetFlags flags = TargetFlags.None, bool retryOnLos = false) - : base(spell.TargetRange, allowGround, flags) + public SpellTarget( + ITargetingSpell spell, + bool allowGround = false, + TargetFlags flags = TargetFlags.None, + bool retryOnLos = false + ) : base(spell.TargetRange, allowGround, flags) { _spell = spell; _retryOnLos = retryOnLos; @@ -29,11 +33,13 @@ public SpellTarget(ITargetingSpell spell, bool allowGround, TargetFlags flags public ITargetingSpell Spell => _spell; protected override bool CanTarget(Mobile from, StaticTarget staticTarget, ref Point3D loc, ref Map map) - => _canTargetStatic; + => base.CanTarget(from, staticTarget, ref loc, ref map) && _canTargetStatic; - protected override bool CanTarget(Mobile from, Mobile mobile, ref Point3D loc, ref Map map) => _canTargetMobile; + protected override bool CanTarget(Mobile from, Mobile mobile, ref Point3D loc, ref Map map) => + base.CanTarget(from, mobile, ref loc, ref map) && _canTargetMobile; - protected override bool CanTarget(Mobile from, Item item, ref Point3D loc, ref Map map) => _canTargetItem; + protected override bool CanTarget(Mobile from, Item item, ref Point3D loc, ref Map map) => + base.CanTarget(from, item, ref loc, ref map) && _canTargetItem; protected override void OnCantSeeTarget(Mobile from, object o) { diff --git a/Projects/UOContent/Spells/Third/Teleport.cs b/Projects/UOContent/Spells/Third/Teleport.cs index 7bcbb6fa98..4b8aad856f 100644 --- a/Projects/UOContent/Spells/Third/Teleport.cs +++ b/Projects/UOContent/Spells/Third/Teleport.cs @@ -139,7 +139,7 @@ public override bool CheckCast() public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } } diff --git a/Projects/UOContent/Spells/Third/WallOfStone.cs b/Projects/UOContent/Spells/Third/WallOfStone.cs index 8b2e5621ea..f1c94c6fdb 100644 --- a/Projects/UOContent/Spells/Third/WallOfStone.cs +++ b/Projects/UOContent/Spells/Third/WallOfStone.cs @@ -56,7 +56,7 @@ public void Target(IPoint3D p) public override void OnCast() { - Caster.Target = new SpellTarget(this); + Caster.Target = new SpellTarget(this, allowGround: true); } } diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index 02d8700a08..5f07339eb2 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -38,10 +38,12 @@ false - + - + + + diff --git a/README.md b/README.md index 366f3493cb..348dae4ac9 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ModernUO [![Discord](https://img.shields.io/discord/751317910504603701?logo=disc ## Requirements #### Supported Operating Systems -[![Windows 10/11/2019/2022](https://img.shields.io/badge/-server%202022-0078D6?logo=windows&logoColor=0078D6&labelColor=222222)](https://www.microsoft.com/en-US/evalcenter/evaluate-windows-server-2022) +[![Windows 10/11/2019/2022](https://img.shields.io/badge/-server%202022-3c78d5?labelColor=222222&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHJvbGU9ImltZyIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGl0bGU+V2luZG93czwvdGl0bGU+PHBhdGggZD0iTTAsMEgxMS4zNzdWMTEuMzcySDBaTTEyLjYyMywwSDI0VjExLjM3MkgxMi42MjNaTTAsMTIuNjIzSDExLjM3N1YyNEgwWm0xMi42MjMsMEgyNFYyNEgxMi42MjMiIGZpbGw9IiMzYzc4ZDUiLz48L3N2Zz4=)](https://www.microsoft.com/en-US/evalcenter/evaluate-windows-server-2022) ![MacOS 13+](https://img.shields.io/badge/-sonoma-222222?logo=apple&logoColor=white&labelColor=222222) [![Debian 11+](https://img.shields.io/badge/-bookworm-A81D33?logo=debian&logoColor=A81D33&labelColor=222222)](https://www.debian.org/distrib/) [![Ubuntu 20+ LTS](https://img.shields.io/badge/-22LTS-E95420?logo=ubuntu&logoColor=E95420&labelColor=222222)](https://ubuntu.com/download/server) @@ -31,24 +31,24 @@ ModernUO [![Discord](https://img.shields.io/discord/751317910504603701?logo=disc #### Required Frameworks ##### All Operating Systems -[![.NET](https://img.shields.io/badge/-8.0.6-5C2D91?logo=.NET&logoColor=white&labelColor=222222)](https://dotnet.microsoft.com/download/dotnet/8.0) +[![.NET](https://img.shields.io/badge/-8.0.8-5C2D91?logo=.NET&logoColor=white&labelColor=222222)](https://dotnet.microsoft.com/download/dotnet/8.0) ##### Windows [![VC++ Redistributable 17](https://img.shields.io/badge/-Redist%2017-00599C?logo=cplusplus&logoColor=white&labelColor=222222)](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2015-2017-2019-and-2022) #### Development [![git](https://img.shields.io/badge/-git-F05032?logo=git&logoColor=F05032&labelColor=222222)](https://git-scm.com/downloads) -[![.NET](https://img.shields.io/badge/-%208.0.301%20SDK-5C2D91?logo=.NET&logoColor=white&labelColor=222222)](https://dotnet.microsoft.com/download/dotnet/8.0) +[![.NET](https://img.shields.io/badge/-%208.0.401%20SDK-5C2D91?logo=.NET&logoColor=white&labelColor=222222)](https://dotnet.microsoft.com/download/dotnet/8.0) #### Supported IDEs -

Jetbrains Rider 2024.1+spaceVSCodespaceVisual Studio 2022 v17.10+

diff --git a/Rules.ruleset b/Rules.ruleset index 32bd4e62c9..84b14555f2 100644 --- a/Rules.ruleset +++ b/Rules.ruleset @@ -1,496 +1,499 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/version.json b/version.json index 43964a515a..7870d130e7 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.13.4" + "version": "0.13.6" }